Emergency Hotline: Call 1-844-363-1423 (United We Dream Hotline)
ICE Encounter

Designing for Stressed Users

Immigration advocacy maps serve users who may be:

  • Under immediate enforcement pressure
  • Unfamiliar with technology
  • Using low-end mobile devices
  • In areas with poor connectivity

Every interaction must be immediate, intuitive, and privacy-preserving.


Privacy-Preserving Search

The Problem

Commercial geocoders (Google Maps API) transmit sensitive location queries to external servers, creating surveillance risks.

Solution: Open-Source Geocoding

Service Description
Nominatim OSM-based, multi-language
Pelias Elasticsearch-based, fast autocomplete
Photon Komoot-hosted, free

Nominatim Implementation

async function searchLocation(query) {
  const url = `https://nominatim.openstreetmap.org/search?` +
    `q=${encodeURIComponent(query)}` +
    `&format=json` +
    `&limit=5` +
    `&countrycodes=us`;

  const response = await fetch(url, {
    headers: {
      'Accept-Language': navigator.language
    }
  });

  return response.json();
}

Multi-Language Support

Nominatim parses Accept-Language headers and searches localized name:* tags:

// User's browser set to Spanish
// Nominatim returns Spanish place names
fetch(url, {
  headers: {
    'Accept-Language': 'es'
  }
});

"Find Nearest" Without Tracking

Client-Side Distance Calculation

import * as turf from '@turf/turf';

function findNearestFacility(userLat, userLng, facilities) {
  const userPoint = turf.point([userLng, userLat]);

  let nearest = null;
  let minDistance = Infinity;

  facilities.features.forEach(facility => {
    const distance = turf.distance(userPoint, facility, {units: 'miles'});
    if (distance < minDistance) {
      minDistance = distance;
      nearest = facility;
    }
  });

  // User location NEVER leaves the browser
  return { facility: nearest, distance: minDistance };
}

Geolocation Request

function getUserLocation() {
  return new Promise((resolve, reject) => {
    if (!navigator.geolocation) {
      reject(new Error('Geolocation not supported'));
      return;
    }

    navigator.geolocation.getCurrentPosition(
      (position) => {
        // Process locally, never transmit
        resolve({
          lat: position.coords.latitude,
          lng: position.coords.longitude
        });
      },
      (error) => reject(error),
      { enableHighAccuracy: false } // Coarse location is sufficient
    );
  });
}

Filtering & Layer Controls

Layer Toggle UI

<div class="layer-controls">
  <label>
    <input type="checkbox" checked data-layer="facilities">
    ICE Facilities
  </label>
  <label>
    <input type="checkbox" checked data-layer="checkpoints">
    Checkpoints
  </label>
  <label>
    <input type="checkbox" data-layer="287g">
    287(g) Jurisdictions
  </label>
</div>
document.querySelectorAll('[data-layer]').forEach(input => {
  input.addEventListener('change', (e) => {
    const layerId = e.target.dataset.layer;
    const visible = e.target.checked;

    if (visible) {
      map.addLayer(layers[layerId]);
    } else {
      map.removeLayer(layers[layerId]);
    }
  });
});

Filter by Type

function filterFacilities(type) {
  facilitiesLayer.eachLayer(layer => {
    const facilityType = layer.feature.properties.facility_type;

    if (type === 'all' || facilityType === type) {
      layer.setStyle({ opacity: 1, fillOpacity: 0.7 });
    } else {
      layer.setStyle({ opacity: 0.2, fillOpacity: 0.1 });
    }
  });
}

Marker Clustering

For Dense Point Data

const clusterGroup = L.markerClusterGroup({
  maxClusterRadius: 50,
  spiderfyOnMaxZoom: true,
  showCoverageOnHover: false,
  iconCreateFunction: (cluster) => {
    const count = cluster.getChildCount();
    const size = count < 10 ? 'small' : count < 100 ? 'medium' : 'large';

    return L.divIcon({
      html: `<div>${count}</div>`,
      className: `cluster-icon cluster-${size}`,
      iconSize: [40, 40]
    });
  }
});

checkpointsLayer.addTo(clusterGroup);
clusterGroup.addTo(map);

Cluster Styling

.cluster-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  font-weight: bold;
  color: white;
}

.cluster-small { background: #3498db; }
.cluster-medium { background: #e67e22; }
.cluster-large { background: #e74c3c; }

Progressive Disclosure Popups

Initial Click: Summary

function createPopup(feature) {
  const props = feature.properties;

  return `
    <div class="popup-summary">
      <h3>${props.name}</h3>
      <p>${props.type} • ${props.operator}</p>
      <button onclick="showDetails('${props.id}')">
        More Information
      </button>
    </div>
  `;
}

Expanded View: Details

function showDetails(facilityId) {
  const facility = getFacilityById(facilityId);
  const props = facility.properties;

  document.querySelector('.popup-content').innerHTML = `
    <div class="popup-details">
      <h3>${props.name}</h3>

      <table>
        <tr><td>Type:</td><td>${props.facility_type}</td></tr>
        <tr><td>Capacity:</td><td>${props.capacity}</td></tr>
        <tr><td>Population:</td><td>${props.current_population}</td></tr>
        <tr><td>Standards:</td><td>${props.standards}</td></tr>
      </table>

      <div class="popup-actions">
        <a href="${props.kyr_link}">Know Your Rights</a>
        <a href="${props.visitation_link}">Visitation Info</a>
      </div>
    </div>
  `;
}

Mobile-First Interactions

Thumb Zone Design

Primary controls must be reachable with one thumb:

.map-controls {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 10px;
}

.map-control-btn {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  touch-action: manipulation;
}

Bottom Sheet for Filters

<div class="bottom-sheet" id="filter-sheet">
  <div class="sheet-handle"></div>
  <div class="sheet-content">
    <h3>Filter Map</h3>
    <!-- Filter controls -->
  </div>
</div>
// Swipe up to expand, swipe down to collapse
const sheet = document.getElementById('filter-sheet');
let startY;

sheet.addEventListener('touchstart', (e) => {
  startY = e.touches[0].clientY;
});

sheet.addEventListener('touchmove', (e) => {
  const deltaY = startY - e.touches[0].clientY;
  // Handle swipe gesture
});

Scroll-Trap Prevention

The Problem

Full-width maps on mobile can trap users who are trying to scroll past them.

Solution: Conditional Dragging

const isMobile = L.Browser.mobile;

const map = L.map('map', {
  dragging: !isMobile,
  tap: !isMobile,
  scrollWheelZoom: false
});

// Add explicit interaction instructions
if (isMobile) {
  L.control.attribution({
    prefix: 'Use two fingers to pan and zoom'
  }).addTo(map);
}

Enable on Focus

// Enable dragging when map is focused
map.on('focus', () => {
  map.dragging.enable();
});

map.on('blur', () => {
  if (isMobile) {
    map.dragging.disable();
  }
});

Loading States

Skeleton Screen

<div class="map-container">
  <div class="map-skeleton" id="map-loading">
    <div class="skeleton-pulse"></div>
    <p>Loading map...</p>
  </div>
  <div id="map" style="display: none;"></div>
</div>
map.on('load', () => {
  document.getElementById('map-loading').style.display = 'none';
  document.getElementById('map').style.display = 'block';
});

Progress Indicator

let loadedLayers = 0;
const totalLayers = 4;

function updateProgress() {
  loadedLayers++;
  const percent = (loadedLayers / totalLayers) * 100;
  document.querySelector('.progress-bar').style.width = `${percent}%`;

  if (loadedLayers === totalLayers) {
    hideLoadingOverlay();
  }
}

Touch Target Sizes

Minimum Sizes

Element Minimum Size
Buttons 44x44px
Checkboxes 44x44px tap area
Links 44px height
Cluster icons 40x40px minimum

Popup Link Styling

.popup-content a {
  display: inline-block;
  padding: 12px 16px;
  min-height: 44px;
  line-height: 20px;
  background: #2196f3;
  color: white;
  text-decoration: none;
  border-radius: 4px;
}

Error Handling

Graceful Degradation

async function loadMapData() {
  try {
    const facilities = await fetch('/data/facilities.geojson');
    displayFacilities(await facilities.json());
  } catch (error) {
    showError(`
      <p>Unable to load map data.</p>
      <p><a href="/facilities-list/">View facility list</a></p>
    `);
  }
}

Offline Detection

window.addEventListener('offline', () => {
  showNotification('You are offline. Some features may be unavailable.');
});

window.addEventListener('online', () => {
  hideNotification();
  refreshMapData();
});

Related Resources