ferries: a layer of their own, not unpaved roads #34

Merged
art merged 1 commit from night/issue-6-ferry-lines into main 2026-08-02 06:00:24 +00:00
Collaborator

Closes #6


Ferry lines now have their own layer instead of being roads.

Branch: night/issue-6-ferry-lines (not pushed, not merged).

What the data actually says

NVDB publishes a ferry route as an ordinary bilnät row in SverigepaketTP with the
route's name in Farjeled_Farjeledsnamn. There are 215 such rows, 1 846 km, 86 named
routes
, and nothing else in that file marks a ferry — Vagtyp_typ has no ferry value,
and the flattened TP package keeps only the name from NVDB's own Färjeled feature type.
So the road importer imported all 215 as roads.

It imported them as unpaved roads, which is the second half of the bug: a ferry row
has no Slitlager_Slitlagertyp, and the surface CASE ended in ELSE 'unpaved'. Both
reported features check out exactly — Ekeröleden is FunkVagklass 6 → tertiary +
unpaved, and the pair at Nynäshamn are Gotlands linje / Polenfärjan / Lettlandfärjan /
Nynäshamn–Hanko, all FunkVagklass 3 → secondary + unpaved. Only 306 bilnät rows have
no Slitlager at all and 215 of them were the ferries.

What I changed

A new shared layer ferries (conf/common.yaml), z5–14, geom + name. Not an eighth
road_class: road_class is a ladder of importance that the zoom filters read as one
(road_class <= 'tertiary'), and a ferry has no rung on it — NVDB's own functional class
puts Stockholm–Helsingfors at 7 and a 100 m cable ferry at 5, because it describes the road
on the shore. Same argument the config already makes against a track class, only
stronger. It is also drawn differently and generalizes differently, which is what a layer
is for.

No class or importance column, deliberately. A ferry route is a whole feature, not a
segment split at intersections, so unlike roads this layer can use min_length_pixels
set to 1, one screen pixel of route. That produces the whole ladder out of geometry with
nothing invented: 86 routes at z14 → 71 at z8 → 34 at z5, i.e. cable ferries at high zoom,
vägfärja crossings in the middle, only the long-haul Baltic lines at z5. NVDB does state
one real distinction (statlig vägfärja with a numbered länsväg, 38 routes, versus enskild
commercial line, 44) and I left it out on purpose: no other source here is known to draw
it, length separates them in practice, and inventing a shared vocabulary two of three
countries can't be checked against is what this project's config comments keep warning
against. It is written down in conf/common.yaml so the option is not lost.

conf/semap.yaml — the ferries layer (the exact complement of the roads WHERE), and
two changes to roads: ferries excluded, and surface is now NULL when Slitlager is
absent instead of asserting unpaved. That second one is a separate small fix riding
along; it affects the 91 remaining unknown-surface roads and is one line if you want it
gone.

conf/world.yamlferries added to the map source. html/style.json — a
ferries dashed mid-blue line drawn between waterways and the road casings, and
ferry_labels from z7 (63 layers now, was 61). CLAUDE.md / docs/country-{sweden,
iceland,poland}.md
updated.

One doc bug fixed on the way: min_length_pixels is documented as "default 2px" in
both CLAUDE.md and the layer struct, but the code has always treated 0 as "no filter"
and every road/rail layer depends on that. The comments were wrong, not the code — I fixed
the comments rather than the behaviour, since implementing a default of 2 would silently
put a length filter on every road network.

How I verified it

  • make build, cd mapsrv && go test ./..., gl-style-validate html/style.json — all
    clean.
  • Ran the real import: importer/importer -layer ferries conf/semap.yaml, 3.5 s. Row
    counts per zoom 215/118/118/116/114/109/95/79/54/39 (z14→z5), 86 distinct names down to
    34, no NULL names, no invalid geometry, all SRID 3857 LINESTRING. Partition
    map_ferries_14_se attached with its gist index adopted by the parent.
  • make import-world (2 s) so zxy_map calls zxy_map_ferries; decoded
    zxy_map_ferries(9,279,156) and found Gotlands linje / country=se in the tile.
  • Ran the amended roads query against the GeoPackage without importing it: 3 397 786 rows
    (= 3 398 001 live − 215), 91 NULL surfaces, paved/unpaved split unchanged.
  • Drove Chrome over CDP (real time, no virtual-time budget) at the reported location.
    Ekeröleden renders as a dashed blue line with its label; z6 over the Baltic shows the
    long-haul lines without noise. Screenshots were with the roads layers hidden, because:

Left undone

Sweden's roads layer has not been reimported, so the 215 ferries are still in
map_roads_* and still render as unpaved roads underneath the new ferry line. That needs
make import-se (hours — a 3.4M-segment maxz read plus the full cascade), which the night
run is not allowed to start. Nothing else is pending on the database side: map_ferries_*
and zxy_map_ferries are built and zxy_map calls them. The tree is back on main, whose
style.json has no ferry layers yet, so the running server draws nothing new until the
branch is merged — the data is there waiting.

Iceland and Poland get no ferry partition and that is correct, not an omission — IS 50V has
the vocabulary (einingvegakerfis = 7, Ferja) but zero rows in this delivery, and BDOT10k
SKJZ has no ferry attribute or class at all. Both are written up in their country docs.

Two judgement calls a reviewer might want to overrule, neither of which needs a reimport of
anything but this layer: min_length_pixels: 1 (2 would delay each route by about one zoom
level) and the decision not to carry the statlig/enskild distinction.

Closes #6 --- Ferry lines now have their own layer instead of being roads. Branch: `night/issue-6-ferry-lines` (not pushed, not merged). ## What the data actually says NVDB publishes a ferry route as an ordinary `bilnät` row in `SverigepaketTP` with the route's name in `Farjeled_Farjeledsnamn`. There are **215 such rows, 1 846 km, 86 named routes**, and nothing else in that file marks a ferry — `Vagtyp_typ` has no ferry value, and the flattened TP package keeps only the name from NVDB's own Färjeled feature type. So the road importer imported all 215 as roads. It imported them as **unpaved** roads, which is the second half of the bug: a ferry row has no `Slitlager_Slitlagertyp`, and the surface CASE ended in `ELSE 'unpaved'`. Both reported features check out exactly — Ekeröleden is FunkVagklass 6 → `tertiary` + `unpaved`, and the pair at Nynäshamn are Gotlands linje / Polenfärjan / Lettlandfärjan / Nynäshamn–Hanko, all FunkVagklass 3 → `secondary` + `unpaved`. Only 306 `bilnät` rows have no Slitlager at all and 215 of them were the ferries. ## What I changed **A new shared layer `ferries`** (`conf/common.yaml`), z5–14, `geom + name`. Not an eighth `road_class`: `road_class` is a ladder of importance that the zoom filters read as one (`road_class <= 'tertiary'`), and a ferry has no rung on it — NVDB's own functional class puts Stockholm–Helsingfors at 7 and a 100 m cable ferry at 5, because it describes the road on the shore. Same argument the config already makes against a `track` class, only stronger. It is also drawn differently and generalizes differently, which is what a layer is for. **No class or importance column, deliberately.** A ferry route is a whole feature, not a segment split at intersections, so unlike `roads` this layer can use `min_length_pixels` — set to 1, one screen pixel of route. That produces the whole ladder out of geometry with nothing invented: 86 routes at z14 → 71 at z8 → 34 at z5, i.e. cable ferries at high zoom, vägfärja crossings in the middle, only the long-haul Baltic lines at z5. NVDB *does* state one real distinction (statlig vägfärja with a numbered länsväg, 38 routes, versus enskild commercial line, 44) and I left it out on purpose: no other source here is known to draw it, length separates them in practice, and inventing a shared vocabulary two of three countries can't be checked against is what this project's config comments keep warning against. It is written down in `conf/common.yaml` so the option is not lost. **`conf/semap.yaml`** — the `ferries` layer (the exact complement of the roads WHERE), and two changes to `roads`: ferries excluded, and `surface` is now NULL when `Slitlager` is absent instead of asserting `unpaved`. That second one is a separate small fix riding along; it affects the 91 remaining unknown-surface roads and is one line if you want it gone. **`conf/world.yaml`** — `ferries` added to the `map` source. **`html/style.json`** — a `ferries` dashed mid-blue line drawn between `waterways` and the road casings, and `ferry_labels` from z7 (63 layers now, was 61). **CLAUDE.md / docs/country-{sweden, iceland,poland}.md** updated. **One doc bug fixed on the way**: `min_length_pixels` is documented as "default 2px" in both CLAUDE.md and the `layer` struct, but the code has always treated 0 as "no filter" and every road/rail layer depends on that. The comments were wrong, not the code — I fixed the comments rather than the behaviour, since implementing a default of 2 would silently put a length filter on every road network. ## How I verified it - `make build`, `cd mapsrv && go test ./...`, `gl-style-validate html/style.json` — all clean. - Ran the real import: `importer/importer -layer ferries conf/semap.yaml`, 3.5 s. Row counts per zoom 215/118/118/116/114/109/95/79/54/39 (z14→z5), 86 distinct names down to 34, no NULL names, no invalid geometry, all SRID 3857 LINESTRING. Partition `map_ferries_14_se` attached with its gist index adopted by the parent. - `make import-world` (2 s) so `zxy_map` calls `zxy_map_ferries`; decoded `zxy_map_ferries(9,279,156)` and found `Gotlands linje` / `country=se` in the tile. - Ran the amended `roads` query against the GeoPackage without importing it: 3 397 786 rows (= 3 398 001 live − 215), 91 NULL surfaces, paved/unpaved split unchanged. - Drove Chrome over CDP (real time, no virtual-time budget) at the reported location. Ekeröleden renders as a dashed blue line with its label; z6 over the Baltic shows the long-haul lines without noise. Screenshots were with the `roads` layers hidden, because: ## Left undone **Sweden's `roads` layer has not been reimported**, so the 215 ferries are still in `map_roads_*` and still render as unpaved roads *underneath* the new ferry line. That needs `make import-se` (hours — a 3.4M-segment maxz read plus the full cascade), which the night run is not allowed to start. Nothing else is pending on the database side: `map_ferries_*` and `zxy_map_ferries` are built and `zxy_map` calls them. The tree is back on `main`, whose `style.json` has no ferry layers yet, so the running server draws nothing new until the branch is merged — the data is there waiting. Iceland and Poland get no ferry partition and that is correct, not an omission — IS 50V has the vocabulary (`einingvegakerfis` = 7, Ferja) but zero rows in this delivery, and BDOT10k `SKJZ` has no ferry attribute or class at all. Both are written up in their country docs. Two judgement calls a reviewer might want to overrule, neither of which needs a reimport of anything but this layer: `min_length_pixels: 1` (2 would delay each route by about one zoom level) and the decision not to carry the statlig/enskild distinction.
NVDB publishes a ferry route as an ordinary bilnät row in SverigepaketTP with the
route name in Farjeled_Farjeledsnamn, so the roads importer imported all 215 of
them (1 846 km, 86 named routes) as roads — and, because a ferry row has no
Slitlager, as *unpaved* roads. That is the Baltic crossed by gravel tracks, which
is what Forgejo #6 reports twice: Ekeröleden as tertiary/unpaved and the Gotland
and Poland lines as secondary/unpaved.

A new shared `ferries` contract (z5–14, geom + name) rather than an eighth
road_class. road_class is a ladder of importance and the zoom filters read it as
one; a ferry has no rung on it, and NVDB proves the point by giving
Stockholm–Helsingfors class 7 and a 100 m cable ferry class 5 — the class
describes the road on the shore. This is the argument enums.road already makes
against `track`, plus the fact that a ferry is drawn and generalized differently.

No class or importance column at all, because none has to be invented: a ferry
route is a whole feature rather than a segment split at intersections, so this is
the one layer that can use min_length_pixels, and one screen pixel of route
produces the ladder by itself — 86 routes at z14, 71 at z8, 34 at z5, from cable
ferries up to Trelleborg–Rostock. NVDB does state a real distinction (38 statlig
vägfärja carrying a numbered länsväg vs 44 enskild commercial lines) and it is
deliberately not carried: no other source here is known to draw it and length
separates the two in practice. The reasoning is in the contract if that changes.

Also, separately: `surface` is now NULL when Slitlager is absent instead of
claiming 'unpaved'. Absent was how the ferries got their gravel; the 91 rows that
remain after they leave are genuinely unrecorded and now say so.

Verified: the ferries layer imported for real (3.5 s) — 215/118/118/116/114/109/
95/79/54/39 rows z14→z5, no NULL names, no invalid geometry, partition attached
with its gist index adopted; import-world so zxy_map calls zxy_map_ferries, and
zxy_map_ferries(9,279,156) decodes to Gotlands linje/se; the amended roads query
run against the GeoPackage returns exactly 3 398 001 − 215 rows with 91 NULL
surfaces; gl-style-validate clean; Chrome over CDP shows Ekeröleden as a labelled
dashed line. Sweden's roads still need a reimport to actually lose the 215 — that
is hours and is not done here.

Fixes a stale comment on the way: min_length_pixels is documented as defaulting
to 2 in both CLAUDE.md and the layer struct, but 0 (no filter) has always been the
behaviour and every road and rail layer depends on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
art merged commit 27d03792a5 into main 2026-08-02 06:00:24 +00:00
art deleted branch night/issue-6-ferry-lines 2026-08-02 06:00:24 +00:00
Sign in to join this conversation.
No reviewers
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!34
No description provided.