merge_lines: no way to drop an attribute below the zoom that renders it (roads.level costs +16% chains at z11) #167

Open
opened 2026-08-19 17:39:29 +00:00 by art-bot · 0 comments
Collaborator

merge_lines groups by every non-geometry attribute, which is what makes the grouping
safe (nothing a filter, a rollup or the style can see is blurred together) and also what
makes a sparse, frequently-changing attribute expensive: every place the value changes
along a road cuts a merged chain in two, and a chain is two more pinned endpoints at every
zoom below.

roads.level (#164, closing #18) is the first attribute where this is visible. Measured on
the z11 merge, against the live database:

country chains without level with level
se 69 420 80 746 +16.3%
pl 88 768 99 064 +11.6%

Those extra chains buy nothing where they are paid. Grade separation is not legible below
about z13 — a bridge is metres long — while merge_lines runs from z12 down
(merge_from_zoom: 12). The attribute is needed at z13/z14 and is dead weight everywhere
the merge operates.

What is missing is a way to say "this attribute stops varying below zoom N": drop it
from the merge GROUP BY and write NULL (or a declared floor value) into the cascade table
from that zoom down. enum_rollups is the nearest existing mechanism and does not fit — it
is polygon-shaped (ST_Union / ST_Dump, re-unioning same-class neighbours) and it
replaces the zoom's whole generalization step rather than modifying the merge.

Sketch: a per-layer/contract key alongside merge_from_zoom, e.g.

    merge_drop:
      - attr: level
        zoom: 12        # at this zoom and below, group without it and emit NULL

mergeLinesSQL already builds its column list and GROUP BY from attrnameslist(), so the
change is local: emit NULL::<type> AS <attr> in the SELECT and leave the attribute out of
the GROUP BY. It needs validateMerge to reject an attribute named by a zoom_filters
where at or below that zoom (the filter would then be reading a NULL it did not expect),
and a decision on whether the style's roads_under_* / roads_over_* minzooms move up to
13 to match — a layer that can never draw at its declared minzoom violates the
minzoom-mirroring invariant in docs/style.md.

Verification would be the same measurement above, plus tile-weight before/after at z11 and
z12, and a re-generalize of roads across the four countries.

Found in the review of !165. The cost is documented in conf/common.yaml next to the
level attribute and in docs/generalization.md under merge_lines; nothing is broken,
this is a generalization efficiency gap.

`merge_lines` groups by **every** non-geometry attribute, which is what makes the grouping safe (nothing a filter, a rollup or the style can see is blurred together) and also what makes a sparse, frequently-changing attribute expensive: every place the value changes along a road cuts a merged chain in two, and a chain is two more pinned endpoints at every zoom below. `roads.level` (#164, closing #18) is the first attribute where this is visible. Measured on the z11 merge, against the live database: | country | chains without `level` | with `level` | | |---|---|---|---| | se | 69 420 | 80 746 | **+16.3%** | | pl | 88 768 | 99 064 | **+11.6%** | Those extra chains buy nothing where they are paid. Grade separation is not legible below about z13 — a bridge is metres long — while `merge_lines` runs from z12 down (`merge_from_zoom: 12`). The attribute is needed at z13/z14 and is dead weight everywhere the merge operates. **What is missing** is a way to say "this attribute stops varying below zoom N": drop it from the merge `GROUP BY` and write NULL (or a declared floor value) into the cascade table from that zoom down. `enum_rollups` is the nearest existing mechanism and does not fit — it is polygon-shaped (`ST_Union` / `ST_Dump`, re-unioning same-class neighbours) and it *replaces* the zoom's whole generalization step rather than modifying the merge. Sketch: a per-layer/contract key alongside `merge_from_zoom`, e.g. ```yaml merge_drop: - attr: level zoom: 12 # at this zoom and below, group without it and emit NULL ``` `mergeLinesSQL` already builds its column list and `GROUP BY` from `attrnameslist()`, so the change is local: emit `NULL::<type> AS <attr>` in the SELECT and leave the attribute out of the `GROUP BY`. It needs `validateMerge` to reject an attribute named by a `zoom_filters` `where` at or below that zoom (the filter would then be reading a NULL it did not expect), and a decision on whether the style's `roads_under_*` / `roads_over_*` minzooms move up to 13 to match — a layer that can never draw at its declared minzoom violates the minzoom-mirroring invariant in `docs/style.md`. Verification would be the same measurement above, plus tile-weight before/after at z11 and z12, and a re-generalize of `roads` across the four countries. Found in the review of !165. The cost is documented in `conf/common.yaml` next to the `level` attribute and in `docs/generalization.md` under `merge_lines`; nothing is broken, this is a generalization efficiency gap.
art-bot self-assigned this 2026-08-19 17:39:29 +00:00
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#167
No description provided.