34 KiB
Geocoding and Map Issues
This guide covers geocoding, map display, and location-related problems in Changemaker Lite V2.
Overview
Geocoding System
Changemaker Lite V2 uses multi-provider geocoding with automatic fallback:
- Google Geocoding API - Most accurate, requires API key
- Mapbox Geocoding API - Good quality, requires API key
- Nominatim (OpenStreetMap) - Free, no key required
- ArcGIS Geocoding Service - Good for North America
- Photon (OpenStreetMap) - Free alternative
- HERE Geocoding API - Paid option
Geocoding Queue
- BullMQ queue - Async geocoding for bulk imports
- Rate limiting - Respects provider rate limits
- Retry logic - Auto-retry failed geocodes
- Priority - Manual geocodes prioritized over bulk
Map Display
- Leaflet.js - Open-source map library
- OpenStreetMap tiles - Free map tiles
- Circle markers - Color-coded by cut assignment
- Polygon overlays - Cut boundaries
- Geolocate - Find user's current location
Geocoding Failures
Address Not Found
Severity: 🟡 Medium
Symptoms
Location shows null latitude/longitude after geocoding attempt.
API logs:
WARN Geocoding failed for address: "123 Fake St, Nowhere": No results from any provider
Common Causes
- Invalid address - Address doesn't exist
- Typo - Misspelled street/city/postal code
- Incomplete address - Missing city or postal code
- Wrong country - Address in different country
- Rural address - Not in geocoding databases
Solutions
Solution 1: Verify address format
# Good address format (Canadian):
123 Main Street, Toronto, ON M5H 2N2
# Good address format (US):
123 Main Street, New York, NY 10001
# Bad formats:
123 Main # Missing city/postal
Main Street # Missing number
Toronto # Too vague
Solution 2: Test address manually
# Test via Nominatim (no API key needed)
curl "https://nominatim.openstreetmap.org/search?q=123+Main+Street,Toronto,ON&format=json"
# Should return array with results
# If empty, address not found
Solution 3: Try alternative formats
# If "123 Main Street, Toronto ON M5H 2N2" fails, try:
# - "123 Main St, Toronto ON M5H2N2" (no space in postal)
# - "123 Main Street, Toronto Ontario M5H 2N2" (full province)
# - "123 Main Street, M5H 2N2" (postal code only)
# - "M5H 2N2" (postal code geocoding)
Solution 4: Check geocoding logs
# View detailed geocoding attempts
docker compose logs api | grep "Geocoding\|geocode"
# Shows:
# Trying provider: google
# Google geocoding failed: Invalid request
# Trying provider: nominatim
# Nominatim geocoding succeeded
Solution 5: Manually set coordinates
In admin UI (LocationsPage):
- Find location in table
- Click Edit
- Manually enter lat/lng (from Google Maps)
- Save
Or via SQL:
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "UPDATE \"Location\" SET latitude = 43.65, longitude = -79.38
WHERE address = '123 Main Street';"
Prevention
- Address validation - Validate format before saving
- Postal code lookup - Use postal code if full address fails
- Manual review - Flag failed geocodes for manual review
- Alternative sources - Try multiple address formats
All Providers Failed
Severity: 🟠 High
Symptoms
ERROR Geocoding failed: All providers failed for address: "123 Main St"
All 6 geocoding providers returned no results or errors.
Common Causes
- Network issue - Can't reach external APIs
- Rate limits - All providers rate limited
- Invalid API keys - Google/Mapbox keys invalid
- Bad address - Address truly doesn't exist
- Provider outages - Services down
Solutions
Solution 1: Check network connectivity
# Test DNS resolution
docker compose exec api ping -c 3 nominatim.openstreetmap.org
# Test HTTPS connection
docker compose exec api curl -I https://nominatim.openstreetmap.org
# If fails, network issue
Solution 2: Test each provider manually
# Nominatim (free, no key)
curl "https://nominatim.openstreetmap.org/search?q=123+Main+Street,Toronto&format=json"
# Google (requires GOOGLE_GEOCODING_API_KEY)
curl "https://maps.googleapis.com/maps/api/geocode/json?address=123+Main+Street,Toronto&key=YOUR_KEY"
# Mapbox (requires MAPBOX_API_KEY)
curl "https://api.mapbox.com/geocoding/v5/mapbox.places/123+Main+Street,Toronto.json?access_token=YOUR_KEY"
Solution 3: Check API keys
# Verify API keys in .env
cat .env | grep -E "GOOGLE_GEOCODING_API_KEY|MAPBOX_API_KEY|HERE_API_KEY"
# Should show non-empty values
# If empty, providers requiring keys won't work
Solution 4: Check rate limits
# View geocoding stats
curl http://localhost:4000/api/map/geocoding/stats \
-H "Authorization: Bearer YOUR_TOKEN"
# Shows:
# {
# "totalAttempts": 1523,
# "successful": 1450,
# "failed": 73,
# "byProvider": {
# "google": { "attempts": 500, "successes": 480 },
# "nominatim": { "attempts": 600, "successes": 570 }
# }
# }
Solution 5: Wait and retry
Rate limits reset after time:
- Nominatim: 1 request/second (resets immediately)
- Google: 50 requests/second (resets after 1 second)
- Mapbox: 600 requests/minute (resets after 1 minute)
# Retry geocoding after wait
curl -X POST http://localhost:4000/api/map/locations/LOCATION_ID/geocode \
-H "Authorization: Bearer YOUR_TOKEN"
Prevention
- API key monitoring - Alert on API key errors
- Rate limit tracking - Monitor usage against limits
- Provider rotation - Distribute load across providers
- Graceful degradation - Continue with partial results
Low Confidence Results
Severity: 🟢 Low
Symptoms
Geocoding succeeds but coordinates seem wrong or imprecise.
Example:
- Address: "123 Main Street, Toronto"
- Geocoded to: Center of Toronto (not specific address)
Common Causes
- Ambiguous address - Multiple matches
- Incomplete address - Missing street number
- Rural address - Only city-level precision
- Provider limitation - Provider doesn't have precise data
Solutions
Solution 1: Check geocoding confidence
# View location details
curl http://localhost:4000/api/map/locations/LOCATION_ID \
-H "Authorization: Bearer YOUR_TOKEN"
# Response includes:
# {
# "geocodingProvider": "nominatim",
# "geocodingConfidence": "low", # or "high", "medium"
# "latitude": 43.65,
# "longitude": -79.38
# }
Solution 2: Add more detail to address
# Low confidence:
"Main Street, Toronto"
# Higher confidence:
"123 Main Street, Toronto, ON M5H 2N2"
# Best confidence:
"123 Main Street, Toronto, Ontario M5H 2N2, Canada"
Solution 3: Use postal code geocoding
For Canadian addresses, postal code is often more accurate:
# Update location with postal code
UPDATE "Location"
SET "postalCode" = 'M5H 2N2'
WHERE id = 'LOCATION_ID';
# Re-geocode (will use postal code)
curl -X POST http://localhost:4000/api/map/locations/LOCATION_ID/geocode \
-H "Authorization: Bearer YOUR_TOKEN"
Solution 4: Manually verify on map
In LocationsPage:
- Click location row
- View on map
- If wrong, manually drag marker to correct location
- Save
Solution 5: Flag for review
# Mark low-confidence results for manual review
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT id, address, \"geocodingConfidence\"
FROM \"Location\"
WHERE \"geocodingConfidence\" = 'low'
ORDER BY \"createdAt\" DESC
LIMIT 50;"
Prevention
- Confidence tracking - Store confidence score
- Manual review queue - Review low-confidence results
- Address validation - Validate format before geocoding
- Postal code priority - Use postal code when available
Rate Limit Exceeded
Severity: 🟡 Medium
Symptoms
ERROR Geocoding rate limit exceeded for provider: google
WARN Retrying with next provider: mapbox
Or:
ERROR 429 Too Many Requests from https://maps.googleapis.com/
Common Causes
- Bulk import - Geocoding thousands of addresses at once
- No API key - Free tier has lower limits
- Shared IP - Multiple users on same IP
- Testing - Repeated manual geocodes
Solutions
Solution 1: Check rate limits
Per-provider limits:
| Provider | Free Tier | With API Key |
|---|---|---|
| Nominatim | 1/sec | N/A |
| N/A | 50/sec (or paid limit) | |
| Mapbox | N/A | 600/min |
| ArcGIS | 1000/day | Varies |
| Photon | Unlimited | N/A |
| HERE | N/A | Varies by plan |
Solution 2: Use geocoding queue
For bulk operations:
# Queue all ungeocoded locations
curl -X POST http://localhost:4000/api/map/locations/queue-geocoding \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"batchSize": 100}'
# Queue processes at rate-limit-safe speed
Solution 3: Add API keys
# In .env
GOOGLE_GEOCODING_API_KEY=your-key-here
MAPBOX_API_KEY=your-key-here
# Restart API
docker compose restart api
Solution 4: Distribute across providers
# Check provider usage
curl http://localhost:4000/api/map/geocoding/stats \
-H "Authorization: Bearer YOUR_TOKEN"
# If one provider is overused, system auto-rotates to others
Solution 5: Wait and retry
# Wait for rate limit window to reset
# Nominatim: 1 second
# Google: Check quota reset time
# Mapbox: 1 minute
# Retry failed geocodes
curl -X POST http://localhost:4000/api/map/locations/retry-failed \
-H "Authorization: Bearer YOUR_TOKEN"
Prevention
- API keys - Use paid tiers for higher limits
- Queue system - Respect rate limits automatically
- Provider rotation - Distribute load
- Monitor usage - Alert when approaching limits
Map Display Issues
Map Not Loading
Severity: 🟠 High
Symptoms
Map container shows blank white/gray box. No tiles loaded.
Browser console:
Error loading tile: https://tile.openstreetmap.org/...
Failed to load resource: net::ERR_BLOCKED_BY_CLIENT
Common Causes
- Ad blocker - Blocking OSM tile requests
- Network issue - Can't reach tile server
- CSP headers - Content Security Policy blocking
- Leaflet CSS missing - Styles not imported
Solutions
Solution 1: Disable ad blocker
- Disable ad blocker for your site
- Or whitelist
*.openstreetmap.org - Refresh page
Solution 2: Check network
# Test tile server
curl -I https://tile.openstreetmap.org/0/0/0.png
# Should return 200 OK
# If fails, network or DNS issue
Solution 3: Verify Leaflet CSS
In map component file:
// Must import Leaflet CSS
import 'leaflet/dist/leaflet.css';
Check in browser DevTools:
- Elements tab → Check if
.leaflet-containerhas styles - Network tab → Check if
leaflet.cssloaded
Solution 4: Check CSP headers
In nginx/conf.d/default.conf:
# Allow OSM tiles
add_header Content-Security-Policy "... img-src 'self' data: https://*.openstreetmap.org;";
Solution 5: Try alternative tile provider
// In map component
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
// Or try Carto:
// url="https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png"
/>
Prevention
- Ad blocker warning - Detect and show warning
- Fallback tiles - Multiple tile providers
- Error boundaries - Catch map loading errors
- Clear documentation - Document ad blocker issue
Markers Not Appearing
Severity: 🟡 Medium
Symptoms
Map loads but location markers don't appear.
Common Causes
- No data - No locations fetched
- Null coordinates - Locations not geocoded
- Out of bounds - Markers outside map view
- Rendering error - React component error
Solutions
Solution 1: Check data loaded
// In browser console
console.log('Locations:', locations);
// Should show array of locations with lat/lng
// If empty or undefined, data not loaded
Solution 2: Verify coordinates
# Check locations have coordinates
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT COUNT(*) FROM \"Location\" WHERE latitude IS NOT NULL AND longitude IS NOT NULL;"
# If 0, no locations geocoded
Solution 3: Zoom to markers
// In map component, fit bounds to markers
useEffect(() => {
if (locations.length > 0 && mapRef.current) {
const bounds = locations
.filter(l => l.latitude && l.longitude)
.map(l => [l.latitude, l.longitude]);
if (bounds.length > 0) {
mapRef.current.fitBounds(bounds, { padding: [50, 50] });
}
}
}, [locations]);
Solution 4: Check marker rendering
// Verify CircleMarker component
{locations.map((location) => {
if (!location.latitude || !location.longitude) return null;
return (
<CircleMarker
key={location.id}
center={[location.latitude, location.longitude]}
radius={8}
// ...
/>
);
})}
Solution 5: Check browser console
Look for React errors:
Warning: Each child in a list should have a unique "key" prop
Error: Invalid latitude/longitude
Prevention
- Data validation - Ensure data has coordinates
- Error boundaries - Catch rendering errors
- Loading states - Show loading while fetching
- Empty states - Show message if no data
Cuts Not Rendering
Severity: 🟡 Medium
Symptoms
Cut polygons don't appear on map.
Common Causes
- Invalid GeoJSON - Malformed polygon data
- Wrong coordinate order - GeoJSON uses [lng, lat], Leaflet uses [lat, lng]
- Self-intersecting polygon - Invalid polygon geometry
- Out of bounds - Polygon outside map view
Solutions
Solution 1: Validate GeoJSON
# Check cut geometry
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT id, name, ST_AsGeoJSON(geometry) FROM \"Cut\" WHERE id = 'CUT_ID';"
# Verify format:
# {
# "type": "Polygon",
# "coordinates": [[[lng1, lat1], [lng2, lat2], ...]]
# }
Solution 2: Convert coordinates
// GeoJSON uses [lng, lat]
const geojson = {
type: 'Polygon',
coordinates: [[[-79.38, 43.65], [-79.37, 43.65], ...]]
};
// Convert to Leaflet [lat, lng]
const leafletCoords = geojson.coordinates[0].map(([lng, lat]) => [lat, lng]);
Solution 3: Check for self-intersection
-- Validate polygon geometry
SELECT id, name, ST_IsValid(geometry) as is_valid
FROM "Cut"
WHERE NOT ST_IsValid(geometry);
-- If invalid, show reason
SELECT id, name, ST_IsValidReason(geometry)
FROM "Cut"
WHERE NOT ST_IsValid(geometry);
-- Fix with buffer(0)
UPDATE "Cut"
SET geometry = ST_Buffer(geometry, 0)
WHERE NOT ST_IsValid(geometry);
Solution 4: Zoom to cut
// Fit map to cut bounds
useEffect(() => {
if (cut?.geometry && mapRef.current) {
const coords = cut.geometry.coordinates[0].map(([lng, lat]) => [lat, lng]);
const bounds = L.latLngBounds(coords);
mapRef.current.fitBounds(bounds, { padding: [50, 50] });
}
}, [cut]);
Solution 5: Check Polygon component
// Verify Polygon rendering
<Polygon
positions={coords} // Array of [lat, lng]
pathOptions={{
color: '#3498db',
fillColor: '#3498db',
fillOpacity: 0.2
}}
/>
Prevention
- Geometry validation - Validate on save
- Drawing tools - Use validated drawing library
- Import validation - Check imported geometries
- Error handling - Gracefully handle invalid geometries
GPS Not Working
Severity: 🟡 Medium
Symptoms
Geolocate button doesn't work or shows error.
Browser shows permission prompt but location never loads.
Common Causes
- HTTPS required - Geolocation API requires HTTPS (or localhost)
- Permission denied - User denied location permission
- GPS unavailable - Device has no GPS
- Browser doesn't support - Old browser
Solutions
Solution 1: Check HTTPS
Geolocation API requires:
- HTTPS (https://)
- OR localhost (http://localhost)
- OR 127.0.0.1 (http://127.0.0.1)
# In production, ensure HTTPS
# Via Pangolin tunnel or Cloudflare
Solution 2: Grant permission
- Click lock icon in address bar
- Location → Allow
- Refresh page
- Try geolocate again
Solution 3: Test geolocation API
// In browser console
navigator.geolocation.getCurrentPosition(
(pos) => console.log('Location:', pos.coords),
(err) => console.error('Error:', err)
);
// Errors:
// PERMISSION_DENIED - User denied
// POSITION_UNAVAILABLE - GPS unavailable
// TIMEOUT - Taking too long
Solution 4: Increase timeout
// In geolocate code
navigator.geolocation.getCurrentPosition(
successCallback,
errorCallback,
{
timeout: 10000, // 10 seconds (default: 5000)
enableHighAccuracy: true,
maximumAge: 0
}
);
Solution 5: Fallback to IP geolocation
// If GPS fails, use IP-based location
const fallbackLocation = async () => {
const response = await fetch('https://ipapi.co/json/');
const data = await response.json();
return {
latitude: data.latitude,
longitude: data.longitude
};
};
Prevention
- HTTPS in production - Use secure connection
- Permission prompts - Clear instructions
- Fallback options - IP geolocation as backup
- Error handling - User-friendly error messages
Coordinate Issues
Invalid Lat/Lng
Severity: 🟡 Medium
Symptoms
Error: Invalid latitude/longitude values
Or markers appear in wrong location (ocean, wrong country).
Common Causes
- Swapped coordinates - Latitude and longitude reversed
- Out of range - Latitude > 90 or Longitude > 180
- Wrong sign - Positive instead of negative (or vice versa)
- Decimal precision - Too many/few decimal places
Solutions
Solution 1: Validate ranges
Valid ranges:
- Latitude: -90 to 90
- Longitude: -180 to 180
# Find invalid coordinates
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT id, address, latitude, longitude
FROM \"Location\"
WHERE latitude < -90 OR latitude > 90
OR longitude < -180 OR longitude > 180;"
Solution 2: Check coordinate order
# Common mistake: swapped lat/lng
# Toronto should be:
# Latitude: 43.65 (positive, North)
# Longitude: -79.38 (negative, West)
# If showing as 79.38, -43.65, they're swapped
# Fix:
UPDATE "Location"
SET latitude = longitude, longitude = latitude
WHERE id = 'LOCATION_ID';
Solution 3: Verify hemisphere
For North American locations:
- Latitude: Positive (North)
- Longitude: Negative (West)
# If US/Canada location has positive longitude, wrong sign
UPDATE "Location"
SET longitude = longitude * -1
WHERE country = 'Canada' AND longitude > 0;
Solution 4: Check decimal precision
# Good precision (6 decimals ≈ 0.1m accuracy):
Latitude: 43.651234
Longitude: -79.381234
# Too few decimals (imprecise):
Latitude: 43.65
Longitude: -79.38
# Too many decimals (unnecessary):
Latitude: 43.651234567890
Longitude: -79.381234567890
Solution 5: Visual verification
- Open Google Maps
- Enter coordinates:
43.651234, -79.381234 - Verify location matches address
- If wrong, get correct coordinates from Google Maps
Prevention
- Coordinate validation - Check ranges before save
- Visual preview - Show on map before save
- Import validation - Validate imported coordinates
- Decimal precision - Round to 6 decimals
Out of Bounds Coordinates
Severity: 🟢 Low
Symptoms
Markers appear outside expected area (different country/continent).
Solutions
Solution 1: Set map bounds
// Limit map to expected region
const bounds = L.latLngBounds(
[41.0, -95.0], // Southwest corner
[50.0, -74.0] // Northeast corner (covers eastern Canada/US)
);
<MapContainer
maxBounds={bounds}
maxBoundsViscosity={1.0}
// ...
/>
Solution 2: Filter locations by bounds
// Only show locations in expected region
const filteredLocations = locations.filter(location => {
return location.latitude >= 41 && location.latitude <= 50 &&
location.longitude >= -95 && location.longitude <= -74;
});
Projection Errors (NAR Data)
Severity: 🟡 Medium
Symptoms
Locations imported from NAR data appear in wrong place.
Common Causes
- Wrong projection - NAR uses EPSG:3347 (Lambert), not WGS84
- Missing conversion - Coordinates not converted to lat/lng
- Coordinate swap - BG_X and BG_Y reversed
Solutions
Solution 1: Verify NAR import uses proj4
In api/src/modules/map/locations/nar-import.service.ts:
import proj4 from 'proj4';
// Define EPSG:3347 (NAR projection)
proj4.defs('EPSG:3347',
'+proj=lcc +lat_0=63.390675 +lon_0=-91.86666666666666 ' +
'+lat_1=49 +lat_2=77 +x_0=6200000 +y_0=3000000 ' +
'+ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'
);
// Convert
const [longitude, latitude] = proj4('EPSG:3347', 'WGS84', [bgX, bgY]);
Solution 2: Check coordinate order
NAR Address files:
- BG_X: Easting (X coordinate in meters)
- BG_Y: Northing (Y coordinate in meters)
Conversion order: [BG_X, BG_Y] → [longitude, latitude]
Solution 3: Verify conversion
# Test conversion manually
docker compose exec api node -e "
const proj4 = require('proj4');
proj4.defs('EPSG:3347', '+proj=lcc +lat_0=63.390675 +lon_0=-91.86666666666666 +lat_1=49 +lat_2=77 +x_0=6200000 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs');
// Example Toronto coordinates in EPSG:3347:
const [lng, lat] = proj4('EPSG:3347', 'WGS84', [6458123, 3534567]);
console.log('Lat:', lat, 'Lng:', lng);
// Should be approximately: Lat: 43.65 Lng: -79.38
"
Solution 4: Re-import NAR data
If imported incorrectly:
# Delete bad data
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "DELETE FROM \"Location\" WHERE \"importSource\" = 'NAR';"
# Re-import with correct projection
# Via admin UI: /app/map/locations → NAR Import tab
Prevention
- Projection validation - Test conversion on sample data
- Visual verification - Show import preview on map
- Documentation - Document NAR projection requirements
- Import validation - Check coordinates are in expected range
Queue Issues
Geocoding Queue Stuck
Severity: 🟡 Medium
Symptoms
Locations remain ungeocoded even though queue is running.
Queue shows jobs but they never process.
Solutions
Solution 1: Check queue status
# View queue stats
curl http://localhost:4000/api/map/geocoding/queue/stats \
-H "Authorization: Bearer YOUR_TOKEN"
# Shows:
# {
# "waiting": 150,
# "active": 0, # Should be > 0 if processing
# "completed": 2500,
# "failed": 25
# }
Solution 2: Check worker is running
# Worker should log processing
docker compose logs api | grep -i "geocoding worker\|processing geocode"
# Should show:
# Geocoding worker started
# Processing geocode job for location: abc-123
Solution 3: Restart queue worker
# Restart API (restarts worker)
docker compose restart api
# Check worker started
docker compose logs api | grep "Geocoding worker started"
Solution 4: Check Redis connection
# Test Redis
docker compose exec redis redis-cli -a YOUR_REDIS_PASSWORD ping
# Should return: PONG
# Check queue keys
docker compose exec redis redis-cli -a YOUR_REDIS_PASSWORD keys "bull:geocoding:*"
Solution 5: Manually process stuck jobs
# Retry failed jobs
curl -X POST http://localhost:4000/api/map/geocoding/queue/retry-failed \
-H "Authorization: Bearer YOUR_TOKEN"
# Clean stuck jobs
curl -X POST http://localhost:4000/api/map/geocoding/queue/clean \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"status": "failed", "grace": 86400000}' # Clean failed jobs older than 1 day
Prevention
- Health checks - Monitor worker health
- Dead letter queue - Move repeatedly failed jobs
- Alerting - Alert if queue backed up
- Auto-restart - Restart worker if stuck
Jobs Failing
Severity: 🟡 Medium
Symptoms
Queue shows high failed job count.
Locations remain ungeocoded with error status.
Solutions
Solution 1: View failed jobs
# Get failed job details
curl http://localhost:4000/api/map/geocoding/queue/failed \
-H "Authorization: Bearer YOUR_TOKEN"
# Shows:
# [
# {
# "id": "123",
# "data": { "locationId": "abc", "address": "..." },
# "failedReason": "All providers failed",
# "attemptsMade": 3
# }
# ]
Solution 2: Check error patterns
# Common failure reasons
docker compose logs api | grep "Geocoding failed" | sort | uniq -c
# Example output:
# 45 Geocoding failed: Rate limit exceeded
# 12 Geocoding failed: No results found
# 3 Geocoding failed: Network error
Solution 3: Retry with different settings
# Retry with longer timeout
curl -X POST http://localhost:4000/api/map/locations/LOCATION_ID/geocode \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"timeout": 30000}' # 30 seconds
Solution 4: Manual intervention
For repeatedly failing addresses:
- Open LocationsPage
- Find failed locations
- Review address (fix typos)
- Manually set coordinates if needed
- Or delete if invalid
Prevention
- Retry logic - Auto-retry with exponential backoff
- Error categorization - Permanent vs transient failures
- Manual review queue - Flag for manual review after N attempts
- Address validation - Validate before geocoding
Performance Issues
Slow Geocoding
Severity: 🟡 Medium
Symptoms
Geocoding takes 5-10+ seconds per address.
Bulk imports very slow.
Solutions
Solution 1: Use faster providers first
Provider speed (fastest to slowest):
- Google (with API key) - ~200ms
- Mapbox (with API key) - ~300ms
- Nominatim - ~500ms
- ArcGIS - ~800ms
- Photon - ~1000ms
- HERE - ~400ms
Configure in api/src/modules/map/geocoding/geocoding.service.ts.
Solution 2: Increase concurrency
In geocoding queue worker:
// Increase concurrent geocoding
const worker = new Worker('geocoding', processor, {
concurrency: 5, // Process 5 at a time (default: 1)
limiter: {
max: 50, // Max 50 jobs per second
duration: 1000
}
});
Solution 3: Use bulk geocoding APIs
Some providers offer batch geocoding:
# Google Batch Geocoding (requires Business plan)
# Can geocode up to 100 addresses in one request
Solution 4: Cache results
// Cache geocoding results in Redis
const cacheKey = `geocode:${address}`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const result = await geocode(address);
await redis.setex(cacheKey, 86400, JSON.stringify(result)); // Cache 24h
return result;
Solution 5: Parallel processing
// Geocode multiple addresses in parallel
const addresses = ['123 Main St', '456 Oak Ave', ...];
const results = await Promise.all(
addresses.map(address => geocode(address))
);
Prevention
- Queue system - Don't block UI on geocoding
- Paid tiers - Faster with API keys
- Caching - Cache frequent addresses
- Parallel processing - Process multiple at once
Too Many API Calls
Severity: 🟡 Medium
Symptoms
High API usage on Google/Mapbox.
Approaching or exceeding quota.
Solutions
Solution 1: Monitor usage
# Check geocoding stats
curl http://localhost:4000/api/map/geocoding/stats \
-H "Authorization: Bearer YOUR_TOKEN"
# Track API costs:
# Google: $5 per 1000 requests (after 40k free/month)
# Mapbox: $0.50 per 1000 requests (after 100k free/month)
Solution 2: Use free providers first
Reorder provider priority:
// In geocodingService.ts
const providers = [
'nominatim', // Free (try first)
'photon', // Free
'arcgis', // Free (1000/day)
'google', // Paid (use only if others fail)
'mapbox', // Paid
'here' // Paid
];
Solution 3: Cache aggressively
// Cache geocoding results permanently
const cacheKey = `geocode:${normalizeAddress(address)}`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const result = await geocode(address);
await redis.set(cacheKey, JSON.stringify(result)); // No expiration
return result;
Solution 4: Deduplicate requests
# Before geocoding, check if address already geocoded
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT latitude, longitude FROM \"Location\"
WHERE LOWER(address) = LOWER('123 Main Street, Toronto')
AND latitude IS NOT NULL
LIMIT 1;"
# If exists, copy coordinates instead of geocoding again
Solution 5: Set quota alerts
In Google Cloud Console:
- Navigate to Geocoding API
- Set quota alerts (e.g., 80% of limit)
- Receive email before exceeding quota
Prevention
- Cache everything - Never geocode same address twice
- Free providers first - Use paid only as fallback
- Quota monitoring - Alert before exceeding
- Cost tracking - Monitor API costs monthly
Data Quality
Duplicate Locations
Severity: 🟢 Low
Symptoms
Same address appears multiple times in locations table.
Solutions
Solution 1: Find duplicates
# Find duplicate addresses
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT address, COUNT(*), array_agg(id)
FROM \"Location\"
GROUP BY LOWER(address)
HAVING COUNT(*) > 1;"
Solution 2: Merge duplicates
# Keep oldest, delete newer
# (After reassigning foreign keys to kept record)
DELETE FROM "Location" AS l1
WHERE EXISTS (
SELECT 1 FROM "Location" AS l2
WHERE LOWER(l2.address) = LOWER(l1.address)
AND l2."createdAt" < l1."createdAt"
);
Solution 3: Add unique constraint
model Location {
id String @id @default(uuid())
address String
@@unique([address]) // Prevent duplicates
}
Prevention
- Unique constraints - Database prevents duplicates
- Upsert logic - Update if exists, create if not
- Import validation - Check for duplicates before import
- Case-insensitive comparison - Normalize before checking
Ungeocoded Locations
Severity: 🟡 Medium
Symptoms
Many locations with null latitude/longitude.
Solutions
Solution 1: Count ungeocoded
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT COUNT(*) FROM \"Location\" WHERE latitude IS NULL;"
Solution 2: Queue all ungeocoded
# Via API
curl -X POST http://localhost:4000/api/map/locations/queue-geocoding \
-H "Authorization: Bearer YOUR_TOKEN"
# Queues all locations with null coordinates
Solution 3: View on Data Quality Dashboard
Navigate to /app/map/data-quality:
- Shows geocoding rate
- Lists ungeocoded locations
- Allows bulk geocoding
Solution 4: Export ungeocoded for manual review
# Export to CSV
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "COPY (SELECT id, address, city, \"postalCode\" FROM \"Location\"
WHERE latitude IS NULL) TO STDOUT WITH CSV HEADER" > ungeocoded.csv
Prevention
- Geocode on create - Auto-geocode new locations
- Required coordinates - Don't allow creating without geocoding
- Dashboard monitoring - Track geocoding rate
- Regular cleanup - Periodic geocoding of ungeocoded
Useful Commands
Geocoding Operations
# Geocode single location
curl -X POST http://localhost:4000/api/map/locations/LOCATION_ID/geocode \
-H "Authorization: Bearer YOUR_TOKEN"
# Bulk geocode via queue
curl -X POST http://localhost:4000/api/map/locations/queue-geocoding \
-H "Authorization: Bearer YOUR_TOKEN"
# Check geocoding stats
curl http://localhost:4000/api/map/geocoding/stats \
-H "Authorization: Bearer YOUR_TOKEN"
# Retry failed geocodes
curl -X POST http://localhost:4000/api/map/geocoding/queue/retry-failed \
-H "Authorization: Bearer YOUR_TOKEN"
Database Queries
# Count by geocoding status
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT
COUNT(*) FILTER (WHERE latitude IS NOT NULL) as geocoded,
COUNT(*) FILTER (WHERE latitude IS NULL) as ungeocoded
FROM \"Location\";"
# List ungeocoded
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT id, address FROM \"Location\"
WHERE latitude IS NULL
LIMIT 50;"
# Geocoding provider stats
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT \"geocodingProvider\", COUNT(*)
FROM \"Location\"
WHERE \"geocodingProvider\" IS NOT NULL
GROUP BY \"geocodingProvider\";"
Related Documentation
Geocoding Documentation
- Geocoding Issues - This guide
- Locations Feature - Location management
- Data Quality Dashboard - Monitoring geocoding
Other Troubleshooting
- Common Errors - General errors
- Performance Optimization - Speed improvements
External Resources
Last Updated: February 2026 Version: V2.0 Status: Complete