Thin railways at coarse zoom — it is now the biggest thing in a continental tile #9

Closed
opened 2026-08-01 19:12:55 +00:00 by art-bot · 2 comments
Collaborator

railways is z5–12 with a rail_class ladder of main/branch/siding and no
zoom_filters at all
, so every siding in Sweden and Poland survives to z5 — where a
whole country is a fraction of one tile.

Measured: in the z5 tile covering all of Sweden, railways is 690 kB of 1.37 MB (50%)
for 28 691 features
, against cover's 142 kB after the generalization work. It is the
single largest thing in a continental-scale tile by a wide margin.

Roads solved exactly this problem with the shared road_class ladder and a rung dropped
per zoom (CLAUDE.md, "Generalization strategies" → zoom_filters). Rail has the ladder
and not the filters. This is the cheapest large byte win available and it is a config
change, not code
: siding and then branch should go the way service and minor do,
in conf/common.yaml and both country configs.

Two things to get right:

  • The style's per-layer minzoom has to move with the filters or the layer silently stops
    drawing (CLAUDE.md, "The MapLibre style").
  • Check against #3 first — Polish railways already disappear in places at z10, which is a
    different symptom (probably simplify_line's minimum-length filter) but touches the same
    layer, and a fix for one should not make the other worse.

Verify with -regeneralize rather than a full reimport, and measure the z5 tile before and
after. Background: docs/performance.md, "Where the bytes are now".

`railways` is z5–12 with a `rail_class` ladder of main/branch/siding and **no `zoom_filters` at all**, so every siding in Sweden and Poland survives to z5 — where a whole country is a fraction of one tile. Measured: in the z5 tile covering all of Sweden, `railways` is **690 kB of 1.37 MB (50%) for 28 691 features**, against `cover`'s 142 kB after the generalization work. It is the single largest thing in a continental-scale tile by a wide margin. Roads solved exactly this problem with the shared `road_class` ladder and a rung dropped per zoom (`CLAUDE.md`, "Generalization strategies" → `zoom_filters`). Rail has the ladder and not the filters. **This is the cheapest large byte win available and it is a config change, not code**: `siding` and then `branch` should go the way `service` and `minor` do, in `conf/common.yaml` and both country configs. Two things to get right: - The style's per-layer `minzoom` has to move with the filters or the layer silently stops drawing (`CLAUDE.md`, "The MapLibre style"). - Check against #3 first — Polish railways already disappear in places at z10, which is a different symptom (probably `simplify_line`'s minimum-length filter) but touches the same layer, and a fix for one should not make the other worse. Verify with `-regeneralize` rather than a full reimport, and measure the z5 tile before and after. Background: `docs/performance.md`, "Where the bytes are now".
Author
Collaborator

Fixed on fix/rail-merge-and-pl-classification (commit e6abf49, not merged).

The premise was out of date, and the real cause was something else — worth recording
because it generalises.
The zoom_filters this issue asks for already existed: Phase 5
added rail_class <= 'branch' at z10 and <= 'main' at z8 the day before it was filed,
so the 28 691-feature measurement predates that DB. More rungs would not have helped,
because what survives to z5 is main, and there were 130 183 Swedish main features
averaging 2.0 points and 203 m each
.

ST_SimplifyPreserveTopology preserves the endpoints of whatever row it is handed, and
NJDB delivers the network pre-split, so every 203 m of track pinned two vertices at every
zoom. The cascade was structurally unable to delete a single one. No tolerance, no filter
and no rollup can touch that — only merging the segments back into chains can.

merge_lines does that: group by every non-geometry attribute, ST_LineMerge each group
into maximal chains, then simplify. Sweden's 130 183 stubs become 660 chains averaging
40 km, and z5 goes from 260 366 points to 2 353. It is lossless for rendering — same ink,
fewer features. parallel_merge_pixels: 1 then collapses double track and yard tracks
into the running line (see #3, which shares the fix).

before after
z5 tile 17/10, railways 284 kB 23 kB
z5 tile 17/10, whole map 790 kB 529 kB
z11 1126/602 Stockholm, railways 100 kB 2 086 B
z8 143/84 Poland, railways 11 kB 1 989 B
SE z5 main 130 183 feat / 260 366 pts 161 / 1 303
PL z11 all classes 126 971 feat 10 911

The cascade got faster, not slower — 5 s for all of Sweden z11→z5 — since every level
below the first reads a table one to two orders of magnitude smaller.

Two notes against the issue's checklist:

  • No style change was needed. railways_base/railways_ties carry no class filters,
    and I did not move any zoom filter, so the minzoom-drift trap does not apply here.
  • Checked against #3 first, as asked. They are the same layer but independent faults,
    and the fixes do not fight: #3 was a Polish classification bug that put gaps in main
    lines, and this one is about feature count. Both verified together.

The generalisable lesson (now in docs/performance.md): before tuning tolerances on
a line layer, check avg(ST_NPoints(geom)) at the coarsest zoom. If it is ~2, the layer
is not over-detailed, it is over-divided, and tolerance is the wrong knob. roads
(#10) is the same shape of problem, and merge_lines is built to be reused there.

Fixed on `fix/rail-merge-and-pl-classification` (commit e6abf49, not merged). **The premise was out of date, and the real cause was something else — worth recording because it generalises.** The `zoom_filters` this issue asks for already existed: Phase 5 added `rail_class <= 'branch'` at z10 and `<= 'main'` at z8 the day before it was filed, so the 28 691-feature measurement predates that DB. More rungs would not have helped, because what survives to z5 is `main`, and there were **130 183 Swedish main features averaging 2.0 points and 203 m each**. `ST_SimplifyPreserveTopology` preserves the endpoints of whatever row it is handed, and NJDB delivers the network pre-split, so every 203 m of track pinned two vertices at every zoom. The cascade was structurally unable to delete a single one. No tolerance, no filter and no rollup can touch that — only merging the segments back into chains can. `merge_lines` does that: group by every non-geometry attribute, `ST_LineMerge` each group into maximal chains, then simplify. Sweden's 130 183 stubs become 660 chains averaging 40 km, and z5 goes from 260 366 points to 2 353. It is lossless for rendering — same ink, fewer features. `parallel_merge_pixels: 1` then collapses double track and yard tracks into the running line (see #3, which shares the fix). | | before | after | |---|---|---| | z5 tile 17/10, `railways` | 284 kB | **23 kB** | | z5 tile 17/10, whole `map` | 790 kB | **529 kB** | | z11 1126/602 Stockholm, `railways` | 100 kB | **2 086 B** | | z8 143/84 Poland, `railways` | 11 kB | **1 989 B** | | SE z5 `main` | 130 183 feat / 260 366 pts | 161 / 1 303 | | PL z11 all classes | 126 971 feat | 10 911 | The cascade got *faster*, not slower — 5 s for all of Sweden z11→z5 — since every level below the first reads a table one to two orders of magnitude smaller. Two notes against the issue's checklist: - **No style change was needed.** `railways_base`/`railways_ties` carry no class filters, and I did not move any zoom filter, so the minzoom-drift trap does not apply here. - **Checked against #3 first, as asked.** They are the same layer but independent faults, and the fixes do not fight: #3 was a Polish classification bug that put gaps in main lines, and this one is about feature count. Both verified together. **The generalisable lesson** (now in `docs/performance.md`): before tuning tolerances on a line layer, check `avg(ST_NPoints(geom))` at the coarsest zoom. If it is ~2, the layer is not over-detailed, it is over-*divided*, and tolerance is the wrong knob. `roads` (#10) is the same shape of problem, and `merge_lines` is built to be reused there.
Author
Collaborator

Merged to main as e6abf49 and pushed. Closing.

Merged to `main` as e6abf49 and pushed. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
art/ismap#9
No description provided.