> ## Documentation Index
> Fetch the complete documentation index at: https://www.worldmonitor.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Maps Infrastructure & Geocoding

> Static map assets, country geometry, forward and reverse geocoding services, and boundary overrides used across World Monitor's map layers.

## R2 CDN — `maps.worldmonitor.app`

All 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) |

### Why R2 instead of Vercel?

* Cloudflare bandwidth is free; Vercel charges per GB at scale
* CF Cache Rules cache `/data/`, `/assets/`, `/textures/` etc. for 30 days at edge
* Large files (GeoJSON, PMTiles) don't bloat the Vercel deployment

### Files on R2

| 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) |

### Uploading to R2

```bash theme={null}
# Single file
rclone copyto <local-path> r2:worldmonitor-maps/<filename>

# rclone config note: set no_check_bucket = true (token lacks CreateBucket permission)
```

### CORS

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").

## Country Geometry Service

**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.

### Data Flow

```
countries.geojson (/data/)  ──►  Parse & Index (rebuildCountryIndex)  ──►  countryIndex Map
                                        │
country-boundary-overrides.geojson      │
  (R2 CDN, 3s timeout)     ──►  applyCountryGeometryOverrides  ──►  replace matching polygons
```

1. `countries.geojson` — base polygons with ISO codes and names, served from `/data/` (Vercel)
2. `country-boundary-overrides.geojson` — optional higher-resolution polygons from [Natural Earth](https://www.naturalearthdata.com/), served from R2 CDN (`maps.worldmonitor.app`). Features matched by `ISO3166-1-Alpha-2` (or `ISO_A2`) code; matching features replace the base geometry
3. Base file loads first and the country index is built immediately (service becomes usable). Override file is fetched afterward with a **3-second timeout** — failures are silently ignored. Override lookup uses a `Map<code, Feature>` for O(1) matching

### Indexed Data Structures

| 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 |

### Key Exports

| 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 |

### Name Aliases

Common alternate names are mapped in `NAME_ALIASES`:

```
'dr congo' → CD, 'czech republic' → CZ, 'uae' → AE, 'uk' → GB, 'usa' → US, ...
```

### Political Overrides

`POLITICAL_OVERRIDES` maps sub-national codes to sovereign codes where the app treats them as separate entities (e.g., `CN-TW → TW`).

## Country Boundary Overrides

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](https://github.com/koala73/worldmonitor/issues/1044)).

### How It Works

1. After loading base `countries.geojson`, the app fetches `country-boundary-overrides.geojson` from R2 CDN with a 3-second timeout
2. For each feature in the override file, it matches the country in `countries.geojson` by ISO Alpha-2 code (using a `Map` for O(1) lookup)
3. The override geometry **replaces** the base geometry (both in the raw GeoJSON used for map rendering and in the indexed point-in-polygon data)
4. The override file can contain any number of countries — only matching codes are applied

### Adding a New Country Override

1. Source the boundary from [Natural Earth 50m Admin 0](https://www.naturalearthdata.com/downloads/50m-cultural-vectors/) (depicts de facto boundaries — actual territorial control — not diplomatic claims)
2. Extract the country feature by ISO code and save as GeoJSON
3. Merge into or replace `country-boundary-overrides.geojson`
4. Upload to R2:
   ```bash theme={null}
   rclone copyto public/data/country-boundary-overrides.geojson r2:worldmonitor-maps/country-boundary-overrides.geojson
   ```
5. No code changes needed — the app picks up the new geometry automatically

**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.

### Geopolitical Sensitivity

* Natural Earth shows **de facto** boundaries (who actually controls the territory), not diplomatic claims
* This is the same standard used by most mapping platforms
* When adding overrides for disputed territories, document the source and rationale in the PR description
* The override system does not add or remove territory — it replaces low-resolution outlines with higher-resolution ones from the same authoritative source

## Fallback Bounds

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 Tiles

Basemap tile configuration lives in `src/config/basemap.ts`. See [Map Engine](/docs/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`.

## Common Mistakes

| 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()` |
