docs/maps-and-geocoding.mdx
maps.worldmonitor.appAll large static map files are served from Cloudflare R2, not from Vercel. The R2 bucket worldmonitor-maps is fronted by a CF-proxied custom domain:
| URL | Use |
|---|---|
https://maps.worldmonitor.app/<file> | Production URL (CF-proxied, cached, CORS headers) |
https://pub-8ace9f6a86d74cb2bd5eb1de5590dd9e.r2.dev/<file> | Raw R2 — never use in code (no CF caching, no CORS) |
/data/, /assets/, /textures/ etc. for 30 days at edge| File | Size | Purpose |
|---|---|---|
countries.geojson | ~210 KB | Base country polygons (ISO 3166-1 Alpha-2 coded) |
country-boundary-overrides.geojson | ~600 KB | Higher-resolution Natural Earth boundary overrides |
*.pmtiles | ~80 GB | Self-hosted vector map tiles (when VITE_PMTILES_URL is set) |
# Single file
rclone copyto <local-path> r2:worldmonitor-maps/<filename>
# rclone config note: set no_check_bucket = true (token lacks CreateBucket permission)
R2 does not support wildcard subdomains (https://*.example.com). Each origin must be listed explicitly in the CORS rules. Use r2 bucket cors set or direct curl -X PUT to the R2 API (Wrangler 4.31 may fail with "not well formed").
File: src/services/country-geometry.ts
This service provides all country-level geocoding: point-in-polygon lookups, ISO code resolution, name matching, bounding boxes, and centroids. It loads country boundaries once on first use and indexes them for fast queries.
countries.geojson (/data/) ──► Parse & Index (rebuildCountryIndex) ──► countryIndex Map
│
country-boundary-overrides.geojson │
(R2 CDN, 3s timeout) ──► applyCountryGeometryOverrides ──► replace matching polygons
countries.geojson — base polygons with ISO codes and names, served from /data/ (Vercel)country-boundary-overrides.geojson — optional higher-resolution polygons from Natural Earth, served from R2 CDN (maps.worldmonitor.app). Features matched by ISO3166-1-Alpha-2 (or ISO_A2) code; matching features replace the base geometryMap<code, Feature> for O(1) matching| Structure | Key | Purpose |
|---|---|---|
countryIndex | ISO-2 code | Full geometry + bbox for point-in-polygon |
iso3ToIso2 | ISO-3 code | Alpha-3 → Alpha-2 conversion |
nameToIso2 | lowercase name | Country name → Alpha-2 lookup |
codeToName | ISO-2 code | Code → display name |
sortedCountryNames | — | Regex matchers sorted by name length (longest first) for text extraction |
| Function | Purpose |
|---|---|
preloadCountryGeometry() | Trigger early loading (call at app startup) |
getCountryAtCoordinates(lat, lon) | Point-in-polygon → country code + name |
isCoordinateInCountry(lat, lon, code) | Check if point is inside a specific country |
getCountryNameByCode(code) | ISO-2 → display name |
iso3ToIso2Code(iso3) | ISO-3 → ISO-2 |
nameToCountryCode(text) | Exact name match → ISO-2 |
matchCountryNamesInText(text) | Extract all country names from free text |
getCountryBbox(code) | Bounding box [minLon, minLat, maxLon, maxLat] |
getCountryCentroid(code) | Bbox center, with optional fallback bounds |
resolveCountryFromBounds(lat, lon, bounds) | Resolve overlapping bounding-box regions using geometry |
Common alternate names are mapped in NAME_ALIASES:
'dr congo' → CD, 'czech republic' → CZ, 'uae' → AE, 'uk' → GB, 'usa' → US, ...
POLITICAL_OVERRIDES maps sub-national codes to sovereign codes where the app treats them as separate entities (e.g., CN-TW → TW).
The override mechanism lets us improve individual country boundaries without replacing the entire countries.geojson. This is the foundation for addressing disputed borders (see #1044).
countries.geojson, the app fetches country-boundary-overrides.geojson from R2 CDN with a 3-second timeoutcountries.geojson by ISO Alpha-2 code (using a Map for O(1) lookup)country-boundary-overrides.geojsonrclone copyto public/data/country-boundary-overrides.geojson r2:worldmonitor-maps/country-boundary-overrides.geojson
Example script: scripts/fetch-country-boundary-overrides.mjs downloads the full Natural Earth 50m dataset (~24 MB), extracts country features (currently Pakistan and India), and writes the override file.
For regions where full polygon geometry may not be loaded, ME_STRIKE_BOUNDS in country-geometry.ts provides rectangular bounding boxes for Middle Eastern countries. resolveCountryFromBounds() uses these as a fast first pass, falling back to precise point-in-polygon when multiple bounding boxes overlap.
Basemap tile configuration lives in src/config/basemap.ts. See Map Engine for full details on tile providers (PMTiles, OpenFreeMap, CARTO), themes, and fallback behavior.
PMTiles are also served from R2 via maps.worldmonitor.app, configured through VITE_PMTILES_URL.
| Mistake | Fix |
|---|---|
Using pub-*.r2.dev URLs in code | Always use maps.worldmonitor.app (CF-proxied) |
| Serving large GeoJSON from Vercel | Upload to R2 — Vercel bandwidth is expensive at scale |
| Fetching overrides without a timeout | Always use AbortSignal.timeout — override CDN may be slow or down |
Forgetting POLITICAL_OVERRIDES | Check if the country code needs mapping (e.g., CN-TW → TW) |
| Adding aliases without checking existing | Check NAME_ALIASES and nameToIso2 map first |
Using projection([lon, lat]) without NaN guard | d3 projections can return [NaN, NaN] (truthy) — always check with Number.isFinite() |