# Map Layers

End-to-end layer pipeline for the MapLibre-based map, from OSM tags through
tilemaker to style layers.

## Pipeline

Two tilemaker pipelines produce PMTiles, both emitting an
OpenMapTiles-compatible schema:

| Pipeline        | Config                                        | Processor           | Zoom | Output                |
| --------------- | --------------------------------------------- | ------------------- | ---- | --------------------- |
| World overview  | `data/cli/shared/tilemaker/config.world.json` | `process.world.lua` | 0–9  | single global PMTiles |
| Regional detail | `data/cli/shared/tilemaker/config.json`       | `process.lua`       | 0–14 | per-region PMTiles    |

Regional pipelines extract full OSM detail. The world pipeline is a
deliberately stripped subset — no buildings, housenumbers, POIs, aeroways, or
minor roads — because those are distracting at global zoom and only appear
once a region is downloaded.

MapLibre consumes both through `www/utils/layers.ts`:

- The **default export** returns regional style layers for a given source name.
- The **named `WORLD_LAYERS` export** returns style layers bound to the `world`
  source.

Loading order (`www/components/m-map.ts`):

1. `#loadWorldTiles()` adds the `world` source + `WORLD_LAYERS` on init.
2. `#addRegionalSource()` inserts each regional source's layers **before the
   first world label** (`place-continent`) as regions are downloaded. This
   keeps world place labels above regional water/roads so city/country names
   aren't hidden by regional fills and lines.
3. Bookmark layers are appended and `#ensureBookmarkLayersOnTop()` keeps them
   above everything else.

Final stack (bottom → top): world base → regional layers → world labels →
bookmarks.

## Source-layer coverage

Source-layers below are what tilemaker emits — MapLibre style layers reference
them via `source-layer`.

### Regional (`config.json`)

| Source-layer          | Zoom  | Notes                                                                                                                                                                                               |
| --------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `place`               | 0–14  | country, city, town, suburb, quarter, neighbourhood                                                                                                                                                 |
| `boundary`            | 0–14  | admin_level 2–8                                                                                                                                                                                     |
| `water`               | 6–14  | lakes, rivers, reservoirs                                                                                                                                                                           |
| `ocean`               | 0–14  | loaded from coastline shapefile → `water`                                                                                                                                                           |
| `waterway`            | 8–14  | named rivers (+ `waterway_detail` z12–14)                                                                                                                                                           |
| `landcover`           | 0–14  | grass, wood, sand, ice, farmland                                                                                                                                                                    |
| `landuse`             | 4–14  | residential only                                                                                                                                                                                    |
| `park`                | 11–14 | national_park, nature_reserve                                                                                                                                                                       |
| `transportation`      | 4–14  | roads, rail, ferry, pier, aerialway; rail-family route relations additionally add `is_route`/`route_ref`/`route_name`/`route_colour`/`route_network` (one extra feature per route a way belongs to) |
| `transportation_name` | 8–14  | road + rail names                                                                                                                                                                                   |
| `aeroway`             | 11–14 | runways, taxiways                                                                                                                                                                                   |
| `aerodrome_label`     | 10–14 |                                                                                                                                                                                                     |
| `building`            | 12–14 |                                                                                                                                                                                                     |
| `housenumber`         | 14    |                                                                                                                                                                                                     |
| `poi` / `poi_detail`  | 12–14 | `poi_detail` (rank 5+) written into `poi`                                                                                                                                                           |

### World (`config.world.json`)

| Source-layer | Zoom | Notes                                     |
| ------------ | ---- | ----------------------------------------- |
| `place`      | 0–9  | continent through village                 |
| `boundary`   | 0–9  |                                           |
| `water`      | 4–9  | inland water                              |
| `ocean`      | 0–9  | loaded from coastline shapefile → `water` |
| `waterway`   | 8–9  | named rivers only                         |
| `landcover`  | 0–9  | grass, wood, wetland, sand, rock, ice     |
| `park`       | 7–9  |                                           |

Intentionally omitted from world: transportation (all roads/rail/ferry),
buildings, housenumbers, POIs, aeroways, landuse, mountain peaks.

## Style layers

MapLibre evaluates every layer per frame during pan/zoom. With multiple
regional tile sources loaded simultaneously, the total layer count is
`(regional layers × regions) + world layers + bookmark layers`. Keeping the
per-source layer count low directly improves frame time.

| Source                | Layers |
| --------------------- | ------ |
| Regional (per source) | 39     |
| World                 | 10     |
| Bookmarks             | 5      |

With 3 regional tiles loaded: **132 layers total**.

### Regional layers (`layers.ts` default export)

39 layers, rendered bottom to top:

| #  | Layer ID                        | Type   | Source-layer        | Notes                                                                                               |
| -- | ------------------------------- | ------ | ------------------- | --------------------------------------------------------------------------------------------------- |
| 1  | `landuse-residential`           | fill   | landuse             | z11+, residential / suburb / neighbourhood                                                          |
| 2  | `landcover`                     | fill   | landcover           | **Merged** grass + wood + sand via `match` on class                                                 |
| 3  | `water`                         | fill   | water               | **Merged** normal + intermittent via opacity expression                                             |
| 4  | `landcover-ice-shelf`           | fill   | landcover           | Separate — renders above water                                                                      |
| 5  | `landcover-glacier`             | fill   | landcover           | Separate — opacity ramp z0→z8                                                                       |
| 6  | `landuse`                       | fill   | landcover           | Farmland fill                                                                                       |
| 7  | `landuse_overlay_national_park` | fill   | park                | National parks + nature reserves, opacity ramp z11→z13                                              |
| 8  | `waterway-tunnel`               | line   | waterway            | Dashed + gap-width (unique paint)                                                                   |
| 9  | `waterway`                      | line   | waterway            | Normal waterways                                                                                    |
| 10 | `waterway_intermittent`         | line   | waterway            | Dashed (can't merge — `line-dasharray` not data-driven)                                             |
| 11 | `tunnel_railway_transit`        | line   | transportation      | Kept separate — opacity ramp differs from road tunnels                                              |
| 12 | `building`                      | fill   | building            | z13+ fade-in                                                                                        |
| 13 | `housenumber`                   | symbol | housenumber         | z17+                                                                                                |
| 14 | `road_area_pier`                | fill   | transportation      | Polygon piers                                                                                       |
| 15 | `road_pier`                     | line   | transportation      | Line piers                                                                                          |
| 16 | `road_bridge_area`              | fill   | transportation      | Bridge polygons                                                                                     |
| 17 | `road_path`                     | line   | transportation      | Kept separate — dashed, square cap                                                                  |
| 18 | `tunnel_road`                   | line   | transportation      | **Merged** minor + major tunnels                                                                    |
| 19 | `aeroway-area`                  | fill   | aeroway             | Runway / taxiway polygons                                                                           |
| 20 | `aeroway-taxiway`               | line   | aeroway             | Kept separate — different minzoom / width from runway                                               |
| 21 | `aeroway-runway`                | line   | aeroway             | z4+                                                                                                 |
| 22 | `road`                          | line   | transportation      | **Merged** minor, service, secondary, tertiary, trunk, primary, motorway — class-based `line-width` |
| 23 | `railway`                       | line   | transportation      | **Merged** rail + transit                                                                           |
| 24 | `transit-route-case`            | line   | transportation      | White casing for the route overlay below                                                            |
| 25 | `transit-route`                 | line   | transportation      | Thin coloured line, one treatment for every rail-family mode (metro and long-distance alike)        |
| 26 | `transit-route-label`           | symbol | transportation      | z12+, line placement, route `ref`                                                                   |
| 27 | `transit-station`               | circle | poi                 | z13+, `class=railway` (station/halt/tram_stop)                                                      |
| 28 | `transit-station-label`         | symbol | poi                 | z14+, station name                                                                                  |
| 29 | `waterway-bridge-case`          | line   | waterway            | Different source-layer from road bridges                                                            |
| 30 | `waterway-bridge`               | line   | waterway            |                                                                                                     |
| 31 | `bridge_case`                   | line   | transportation      | **Merged** minor + major bridge casing                                                              |
| 32 | `bridge`                        | line   | transportation      | **Merged** minor + major bridge fill                                                                |
| 33 | `admin_sub`                     | line   | boundary            | State / province boundaries                                                                         |
| 34 | `admin_country`                 | line   | boundary            | z5+ only — world tiles cover z0–5                                                                   |
| 35 | `poi_label`                     | symbol | poi                 | z15+, rank 1 only                                                                                   |
| 36 | `airport-label`                 | symbol | aerodrome_label     | z11+, IATA airports                                                                                 |
| 37 | `road_major_label`              | symbol | transportation_name | z14+, line placement                                                                                |
| 38 | `place_label`                   | symbol | place               | **Merged** city + town + suburb / neighbourhood / quarter — single collision pass                   |
| 39 | `country_label`                 | symbol | place               | Country names, z7–11 (world covers z0–6)                                                            |

### World layers (`WORLD_LAYERS` export)

10 layers, rendered bottom to top:

| #  | Layer ID           | Type   | Source-layer | Notes                                                                          |
| -- | ------------------ | ------ | ------------ | ------------------------------------------------------------------------------ |
| 1  | `landcover`        | fill   | landcover    | **Merged** grass, wood, wetland, sand, rock, ice — class-based color + opacity |
| 2  | `water`            | fill   | water        | Oceans + inland water — above landcover to mask coast-overlapping polygons     |
| 3  | `park`             | fill   | park         | National parks, nature reserves                                                |
| 4  | `waterway`         | line   | waterway     | z6+, named rivers                                                              |
| 5  | `boundary-state`   | line   | boundary     | z3+, admin_level 3–6                                                           |
| 6  | `boundary-country` | line   | boundary     | admin_level 2                                                                  |
| 7  | `place-continent`  | symbol | place        | z0–3, uppercase                                                                |
| 8  | `place-country`    | symbol | place        | maxzoom 7, Medium font for sovereign states                                    |
| 9  | `place-state`      | symbol | place        | z4–8, uppercase                                                                |
| 10 | `place-settlement` | symbol | place        | **Merged** city + town + village, z6–10 (regional takes over at z11)           |

### Bookmark layers

User bookmarks rendered on top of all map layers.
`#ensureBookmarkLayersOnTop()` re-moves them to the top whenever a new tile
source is added.

| Layer ID                        | Type   | Purpose                                 |
| ------------------------------- | ------ | --------------------------------------- |
| `bookmarks-clusters`            | circle | Clustered bookmark pins (5+ points)     |
| `bookmarks-cluster-count`       | circle | Invisible hit area for clusters         |
| `bookmarks-cluster-count-label` | symbol | Cluster count text                      |
| `bookmarks`                     | circle | Individual bookmark pins                |
| `bookmarks-hit`                 | circle | Invisible larger hit area for bookmarks |

## Design decisions

### World and regional dovetail at z5

Country boundaries: regional `admin_country` sets `minzoom: 5` so it only
kicks in where world tiles stop producing meaningful boundary detail. Without
this, country boundaries would render twice from z5 onward.

Other regional layers (water, landcover, …) technically cover z0–14 but are
only attached to the map after a region is downloaded, and at low zooms the
regional source emits very little data due to `SetMinZoomByArea` throttling
in `process.lua`.

### Label handoff between world and regional

Country and settlement labels hand off cleanly instead of overlapping:

| Label      | World                    | Regional              |
| ---------- | ------------------------ | --------------------- |
| country    | `place-country` z0–6     | `country_label` z7–11 |
| settlement | `place-settlement` z6–10 | `place_label` z11–15  |

The regional layer's `minzoom` is set to the same value as the world layer's
`maxzoom` (exclusive), so there is no zoom at which both render. The tradeoff:
if no region is downloaded, country labels disappear at z7+ and settlement
labels disappear at z11+ — acceptable because at those zooms the map has
little else to show without regional data.

### Why some layers are NOT merged

**`line-dasharray` is not data-driven** in MapLibre GL — it's a constant per
layer. This prevents merging layers that need different dash patterns (e.g.
`waterway` vs `waterway_intermittent`, `road_path` vs solid roads).

**Render order matters for bridge casing.** The `bridge_case` layer must
render below `bridge` to create the outline effect. They can't be a single
layer.

**Source-layer boundaries.** Layers using different source-layers (e.g.
`waterway` vs `transportation`) cannot be merged even if they look similar.

**Opacity ramps diverge too much.** Ice-shelf (constant 0.8), glacier (z0→z8
ramp), and national park (z5→z9 ramp) have fundamentally different opacity
behavior. Merging into one expression with multiple zoom stops adds
maintenance cost for minimal gain (3 → 1 layer, but these are simple fills).

### Merge patterns used

**`match` on feature property** — most common. Used for per-class color,
line-width, text-size, opacity. Example: landcover uses
`['match', ['get', 'class'], 'sand', 'rgba(...)', 'hsl(...)']`.

**`interpolate` + `match` composite** — for zoom-dependent values that vary by
class. The outer `interpolate` drives the zoom, and each stop uses a `match`
to return the class-specific value. Example: road `line-width` varies by both
zoom and road class.

**`case` on feature property** — for boolean conditions. Example: water
opacity uses `['case', ['==', ['get', 'intermittent'], 1], 0.7, 1]`.

**`text-size: 0` to hide features** — for per-class minzoom within a merged
symbol layer. MapLibre skips rendering features with text-size 0, so this
effectively implements per-class visibility without separate layers (used in
`place_label`).

### CSS z-index

Container and controls are stacked so map controls sit above the canvas
without breaking DOM order:

- `m-map` container: `z-index: 1`
- Map canvas: `z-index: 1`
- MapLibre controls: `z-index: 10`
- Download button: `z-index: 10`

### Transit route identity

Rail-family route relations (`type=route`, `route` in `train`/`subway`/
`light_rail`/`tram`/`funicular`/`monorail` — bus is out of scope) are scanned
in `relation_scan_function` and pulled onto their member ways via tilemaker's
`NextRelation()`/`FindInRelation()`. Each route a way belongs to becomes an
**additional** `transportation` feature (`is_route=1`, plus `route_ref`/
`route_name`/`route_colour`/`route_network` where OSM supplies them) — the
plain track write is untouched, so hiding the `transit-route-*` layers
client-side reverts to a single grey line. A shared trunk (e.g. several
Cercanías lines running the same track into a terminus) gets one duplicate
feature per route rather than picking one, so none of the lines using that
track lose their branding.

One treatment applies to every rail-family mode alike — metro and
long-distance both render thin, in the route's real colour, with a white
casing underneath (`transit-route-case` under `transit-route`, the same
case-then-line technique as `bridge_case`/`bridge`). Keeps overlapping lines
on shared trunk track legible instead of overwhelming, without muting any
mode to plain grey.

**Ordering pitfall**: `NextRelation()`/`FindInRelation()` only see a way's
relation membership when called early in `way_function` — called later
(e.g. inline in the Railways block, after the function's other `Find()` calls
and branches), they silently return nothing, with no error. Route relations
must be captured into a local table (`railRoutes`) immediately after
`local railway = Find("railway")`, then consumed later where the feature is
actually written.

**Measured cost** (Madrid — Metro + Cercanías, a dense rail network — rebuilt
under identical conditions): baseline 32,387,507 bytes → 32,871,369 bytes with
route identity plus bringing `subway`/`tram`/`monorail`/`funicular` forward
from minzoom 14 to 11 (matching `light_rail`). **+483,862 bytes, +1.49%.**
Rail geometry is a small fraction of a city's total mapped data even
duplicated per overlapping route, but it is not free — plan for roughly this
order of magnitude (more in transit-dense megacities with many overlapping
lines, less almost everywhere else) when re-budgeting archive size.
