#86: give Finland's railway lines the names the register actually has #88

Merged
art merged 2 commits from issue-86-fi-railway-names into main 2026-08-11 07:09:02 +00:00
Owner

Closes #86


#86: give Finland's railway lines the names the register actually has

What changed and why

Finland's railways.name was 100% NULL against Sweden's 73% and Poland's 24%
naming share — R93's parity gap (a label layer only one country populates), found
by the country audit #77.

MTK carries no line names on the rail geometry itself (confirmed against the raw
fi_mtk_muut.gpkg), so there is no new dataset to fetch. The fix is the register
join that stations already uses, wired into railways in conf/fimap.yaml:

  • layer source moves from the raw GeoPackage (db: fi_muut, column
    sijainti_viiva) to the staged tables (db: pg, staging: [fi_rautatie, fi_paikannimi]), the same shape water_bodies and stations use.
  • each rail segment takes the nearest fi_paikannimi point of kohdeluokka 14201
    (the traffic-site label — the register's only railway-name code; verified there
    is no separate line-name code) within 500 m, preferring Finnish per R92's
    DISTINCT ON convention (ORDER BY (kielikoodi='fin') DESC), LIMIT 1.
  • ST_Force2D applied to the geometry per the config's MTK-staging rule; the
    ::rail cast on the enum the db: pg insert path needs.

How it was verified (live DB)

  • acceptance query: SELECT count(*) FROM map_railways_12 WHERE country='fi' AND name IS NOT NULL → 3870 of 18294 rows (21%). Materially greater than
    zero.
  • R92 no-two-labels: no geometry at z12/z11/z10 carries more than one distinct
    name.
  • NULLs: rail_class and electrified all present; only name is nullable by design.
  • geometries: 0 invalid of 18294.
  • partitions: every zoom z3–z12 present; the shared parent table keeps all three
    countries' partitions.
  • decoded tile (z11/1165/592, Helsinki): rail layer carries PASILA, HELSINKI,
    KILO, TIKKURILA etc. — scripts/mvt-decode.py + raw string check.
  • go test ./... in importer/ and mapsrv/: pass.
  • other countries untouched: se/pl partition row counts unchanged.

Notes / trade-offs

  • 21% named is what the register contains — Sweden's 73% is route names
    (Straknamn), Poland's 24% is BDOT10k's own NAZWA; Finland's share is stations,
    so the style's future rail labels will name lines at the sites the lines pass.
  • The merge now groups by name, so a line through several named sites splits
    into per-site chains at coarse zoom. z8 rows 801→1237, z11 2402→2764. This is
    the same behaviour Sweden's named merge exhibits and is the honest consequence
    of carrying a name attribute; documented in docs/country-finland.md.
  • 500 m radius matches the stations layer; a larger radius would name segments
    that pass no site, which would be inventing names.

Nothing left undone; no DB data outside the fi railways partition was touched.

Closes #86 --- #86: give Finland's railway lines the names the register actually has ## What changed and why Finland's `railways.name` was 100% NULL against Sweden's 73% and Poland's 24% naming share — R93's parity gap (a label layer only one country populates), found by the country audit #77. MTK carries no line names on the rail geometry itself (confirmed against the raw `fi_mtk_muut.gpkg`), so there is no new dataset to fetch. The fix is the register join that `stations` already uses, wired into `railways` in `conf/fimap.yaml`: - layer source moves from the raw GeoPackage (`db: fi_muut`, column `sijainti_viiva`) to the staged tables (`db: pg`, `staging: [fi_rautatie, fi_paikannimi]`), the same shape `water_bodies` and `stations` use. - each rail segment takes the nearest `fi_paikannimi` point of kohdeluokka 14201 (the traffic-site label — the register's only railway-name code; verified there is no separate line-name code) within 500 m, preferring Finnish per R92's DISTINCT ON convention (`ORDER BY (kielikoodi='fin') DESC`), `LIMIT 1`. - `ST_Force2D` applied to the geometry per the config's MTK-staging rule; the `::rail` cast on the enum the `db: pg` insert path needs. ## How it was verified (live DB) - acceptance query: `SELECT count(*) FROM map_railways_12 WHERE country='fi' AND name IS NOT NULL` → **3870** of 18294 rows (21%). Materially greater than zero. - R92 no-two-labels: no geometry at z12/z11/z10 carries more than one distinct name. - NULLs: rail_class and electrified all present; only name is nullable by design. - geometries: 0 invalid of 18294. - partitions: every zoom z3–z12 present; the shared parent table keeps all three countries' partitions. - decoded tile (z11/1165/592, Helsinki): rail layer carries PASILA, HELSINKI, KILO, TIKKURILA etc. — `scripts/mvt-decode.py` + raw string check. - `go test ./...` in importer/ and mapsrv/: pass. - other countries untouched: se/pl partition row counts unchanged. ## Notes / trade-offs - 21% named is what the register contains — Sweden's 73% is route names (Straknamn), Poland's 24% is BDOT10k's own NAZWA; Finland's share is stations, so the style's future rail labels will name lines at the sites the lines pass. - The merge now groups by name, so a line through several named sites splits into per-site chains at coarse zoom. z8 rows 801→1237, z11 2402→2764. This is the same behaviour Sweden's named merge exhibits and is the honest consequence of carrying a name attribute; documented in docs/country-finland.md. - 500 m radius matches the stations layer; a larger radius would name segments that pass no site, which would be inventing names. Nothing left undone; no DB data outside the fi railways partition was touched.
Finland's `railways.name` was 100% NULL against Sweden's 73% and Poland's 24%
naming share — R93's parity gap, found by the country audit (#77).

MTK carries no line names on the rail geometry, so the fix is the register join
that `stations` already uses, not new data: each rail segment takes the nearest
`paikannimi` point of kohdeluokka 14201 (the traffic-site label), within the same
500 m the stations join uses, preferring Finnish per R92 so a bilingual site
does not give one line two names. The layer source moves from the raw GeoPackage
(`db: fi_muut`) to the staged tables (`db: pg`, staging fi_rautatie +
fi_paikannimi) — the ST_Force2D that config demands for MTK staging reads, and
the `::rail` cast the pg path needs for the enum.

Verified against the live database: map_railways_12 fi now carries 3 870 named
rows of 18 294 (21%), zero NULLs in rail_class/electrified, zero invalid
geometries, partitions present at every zoom z3–z12, and no feature carries two
names at any zoom (R92's check). A decoded z11 tile over Helsinki carries
PASILA, HELSINKI, KILO, TIKKURILA and friends on the rail layer. The other
countries' partitions are untouched. The merge now groups by name too, so a
line through several named sites splits into per-site chains at coarse zoom —
Sweden's Straknamn does the same with route names; Finland's share is simply
what its register contains.
art-bot left a comment

Review: changes requested

The join itself is right — the register really is the only rail-name source MTK offers, the 500-unit
radius reuses the stations shape, and I confirmed the 3 870 named rows. Two things need fixing
before this merges.

1. The names land in ALL CAPS (blocking)

The 14201 traffic-site register stores its rows in capitals — 620 of 767 — and n.teksti imports
them verbatim:

SELECT count(*) total,
       count(*) FILTER (WHERE name = upper(name) AND length(name) > 3) all_caps
FROM map_railways_12 WHERE country='fi' AND name IS NOT NULL;
-- total 3870, all_caps 3462   (89%)

The PR body quotes this as evidence of success ("rail layer carries PASILA, HELSINKI, KILO,
TIKKURILA") — but that is the shouting map R94 describes, and R94 lands in this same batch
via #80/PR #90. The fix is the idiom #80 already writes for places:

(CASE WHEN n.teksti = upper(n.teksti) THEN initcap(n.teksti) ELSE n.teksti END) AS name

Worth merging #90 first and reusing it. Note initcap leaves S:T MICHEL as S:T Michel (see the
review on #90) — that affects the 14201 rows here too.

The pre-existing stations layer has the same flaw from the same join (349 of 847 fi names are
all-caps). That is not this PR's job, but it wants its own issue.

2. 500 m is not 500 m (fix the comment)

ST_DWithin(p.geom, r.geom, 500) runs against geometry staged in EPSG:3857. At Finland's latitudes
that is 250 m on the ground at 60°N and 171 m at 70°N, not 500 m. Two places assert metres:

  • conf/fimap.yaml:651 — "The 500 m radius is the stations layer's own"
  • docs/country-finland.md:392 — "within the same 500 m the stations join uses"

R88 calls this out explicitly ("a fixed 3857-unit catchment radius likewise means 3.4 km on the
ground in the south and 2.3 km in the north"), and ca3eacb fixed the same class of error for
Iceland's cluster distances a few commits ago. The radius behaves fine — only the quoted figure is
wrong, by 2–3x. Quote it in ground metres per R79, or state it as units and give the ground range.

What checked out

  • 3 870 named rows of 18 294 (21%) — confirmed.
  • I expected ORDER BY (kielikoodi = 'fin') DESC, p.geom <-> r.geom to be able to name a segment
    after a farther site, since language sorts ahead of distance. It cannot, empirically: of the 1 127
    segments where the language-first pick is not the nearest candidate, all 1 127 have an anchor gap
    of exactly 0
    — every one is a co-located bilingual or caps twin of the same site. The ordering is
    safe as written.
  • Merges cleanly onto main, and onto main+#90.
## Review: changes requested The join itself is right — the register really is the only rail-name source MTK offers, the 500-unit radius reuses the `stations` shape, and I confirmed the 3 870 named rows. Two things need fixing before this merges. ### 1. The names land in ALL CAPS (blocking) The 14201 traffic-site register stores its rows in capitals — 620 of 767 — and `n.teksti` imports them verbatim: ```sql SELECT count(*) total, count(*) FILTER (WHERE name = upper(name) AND length(name) > 3) all_caps FROM map_railways_12 WHERE country='fi' AND name IS NOT NULL; -- total 3870, all_caps 3462 (89%) ``` The PR body quotes this as evidence of success ("rail layer carries PASILA, HELSINKI, KILO, TIKKURILA") — but that is the shouting map **R94** describes, and R94 lands in this same batch via #80/PR #90. The fix is the idiom #80 already writes for `places`: ```sql (CASE WHEN n.teksti = upper(n.teksti) THEN initcap(n.teksti) ELSE n.teksti END) AS name ``` Worth merging #90 first and reusing it. Note `initcap` leaves `S:T MICHEL` as `S:T Michel` (see the review on #90) — that affects the 14201 rows here too. The pre-existing `stations` layer has the same flaw from the same join (349 of 847 fi names are all-caps). That is not this PR's job, but it wants its own issue. ### 2. `500 m` is not 500 m (fix the comment) `ST_DWithin(p.geom, r.geom, 500)` runs against geometry staged in EPSG:3857. At Finland's latitudes that is **250 m on the ground at 60°N and 171 m at 70°N**, not 500 m. Two places assert metres: - `conf/fimap.yaml:651` — "The 500 m radius is the stations layer's own" - `docs/country-finland.md:392` — "within the same 500 m the stations join uses" R88 calls this out explicitly ("a fixed 3857-unit catchment radius likewise means 3.4 km on the ground in the south and 2.3 km in the north"), and ca3eacb fixed the same class of error for Iceland's cluster distances a few commits ago. The radius behaves fine — only the quoted figure is wrong, by 2–3x. Quote it in ground metres per R79, or state it as units and give the ground range. ### What checked out - 3 870 named rows of 18 294 (21%) — confirmed. - I expected `ORDER BY (kielikoodi = 'fin') DESC, p.geom <-> r.geom` to be able to name a segment after a farther site, since language sorts ahead of distance. It cannot, empirically: of the 1 127 segments where the language-first pick is not the nearest candidate, **all 1 127 have an anchor gap of exactly 0** — every one is a co-located bilingual or caps twin of the same site. The ordering is safe as written. - Merges cleanly onto main, and onto main+#90.
The 14201 traffic-site register stores its rows in capitals (620 of 767), so
`n.teksti` landed them verbatim — 3 462 of the 3 870 named fi rail rows were
all-caps, the R94 shouting map. Apply the #80 places idiom to the join:
all-caps rows get initcap (HELSINKI → Helsinki, PASILA → Pasila), cased rows
pass through untouched. The documented initcap limit applies — it breaks a
word at every non-alphanumeric character, so S:T MICHEL comes back as
S:T Michel (2 register rows) — which still clears the R94 acceptance query
because the result is no longer all-caps.

Also fix the two comments that quoted the join radius as "500 m".
ST_DWithin runs against EPSG:3857 geometry, so the radius is 500 units —
~250 m on the ground at 60°N, ~171 m at 70°N (R88: nearest-anchor predicates
are safe in Mercator; only the quoted figure was wrong, by 2–3x). Both
comments now quote units with the ground range alongside.

Verified against the live database. The fix lives in the query text, so
map_railways_12 still holds the un-normalized names (total 3870, all_caps
3462, unchanged — an end-to-end check needs a re-import, noted as a
follow-up). Applied to the source register, the CASE drops the all-caps share
of kohdeluokka 14201 from 620 to 0 of 767 rows; the spot-check returns
HELSINKI → Helsinki, PASILA → Pasila, TIKKURILA → Tikkurila, S:T MICHEL →
S:T Michel, and the already-cased S:t Michel untouched. The stations layer
shares the flaw from the same join but is deliberately not fixed here — that
is issue #92.
Author
Owner

Both review points addressed on issue-86-fi-railway-names (commit e458b42, pushed).

1. All-caps names (blocking). The railways join now uses the #80 idiom:
(CASE WHEN n.teksti = upper(n.teksti) THEN initcap(n.teksti) ELSE n.teksti END) AS name
in conf/fimap.yaml. The known initcap limit applies and is documented in the config and in docs/country-finland.md: S:T MICHEL comes back as S:T Michel (2 register rows of the 767), which still clears the R94 check since the result is no longer all-caps. The stations layer keeps the flaw from the same join — not this PR's scope, left for #92.

2. "500 m" is not 500 m. Both comments now quote the radius as 500 units in EPSG:3857 with the ground range alongside (~250 m at 60°N, ~171 m at 70°N, per R88): the railways comment in conf/fimap.yaml and the railways.name paragraph in docs/country-finland.md. The ST_DWithin(..., 500) literal itself is untouched.

Verification (live DB). The fix lives in the query text, so map_railways_12 still holds the un-normalized names — the acceptance query reads total 3870, all_caps 3462, unchanged from the review's measurement. Applied to the source register, the CASE drops the all-caps share of kohdeluokka 14201 from 620 to 0 of 767 rows, and the spot-check returns HELSINKI → Helsinki, PASILA → Pasila, TIKKURILA → Tikkurila, S:T MICHEL → S:T Michel, and the already-cased S:t Michel untouched. An end-to-end confirmation needs a make import re-run (the one change that cannot be checked without re-importing fi_rautatie/fi_paikannimi); the query shape, attrs and attr_names are otherwise unchanged and conf/fimap.yaml parses clean.

Not merged; branch pushed for your review.

Both review points addressed on `issue-86-fi-railway-names` (commit e458b42, pushed). **1. All-caps names (blocking).** The railways join now uses the #80 idiom: `(CASE WHEN n.teksti = upper(n.teksti) THEN initcap(n.teksti) ELSE n.teksti END) AS name` in conf/fimap.yaml. The known initcap limit applies and is documented in the config and in docs/country-finland.md: `S:T MICHEL` comes back as `S:T Michel` (2 register rows of the 767), which still clears the R94 check since the result is no longer all-caps. The stations layer keeps the flaw from the same join — not this PR's scope, left for #92. **2. "500 m" is not 500 m.** Both comments now quote the radius as 500 units in EPSG:3857 with the ground range alongside (~250 m at 60°N, ~171 m at 70°N, per R88): the railways comment in conf/fimap.yaml and the `railways.name` paragraph in docs/country-finland.md. The `ST_DWithin(..., 500)` literal itself is untouched. **Verification (live DB).** The fix lives in the query text, so `map_railways_12` still holds the un-normalized names — the acceptance query reads total 3870, all_caps 3462, unchanged from the review's measurement. Applied to the source register, the CASE drops the all-caps share of kohdeluokka 14201 from 620 to 0 of 767 rows, and the spot-check returns HELSINKI → Helsinki, PASILA → Pasila, TIKKURILA → Tikkurila, S:T MICHEL → S:T Michel, and the already-cased S:t Michel untouched. An end-to-end confirmation needs a `make import` re-run (the one change that cannot be checked without re-importing fi_rautatie/fi_paikannimi); the query shape, attrs and attr_names are otherwise unchanged and conf/fimap.yaml parses clean. Not merged; branch pushed for your review.
Collaborator

Review — ready to merge

Verified against the live DB rather than the diff alone.

Acceptance claim holds. map_railways_12 has 3 870 of 18 294 fi rows named (21%), matching the PR body.

The language rule was the one thing worth attacking, and it survives. ORDER BY (p.kielikoodi = 'fin') DESC, p.geom <-> r.geom leads with a non-distance term, so in principle a farther Finnish name could beat a nearer Swedish one belonging to a different traffic site. Measured — it never does:

picks with a name : 3870
picked farther than pure-nearest : 0
max distance gap : 0.000

876 rows differ in text from a naive nearest pick, and all of them are bilingual rows at identical coordinates. So the fin preference only ever breaks ties at one site, which is exactly the stated intent. No cross-site leakage.

Minor, non-blocking: leading the ORDER BY with the boolean means PostGIS cannot use index-assisted KNN ordering, but ST_DWithin already bounds the candidate set, so it does not matter here.

The documented S:T MICHEL → S:t Michel initcap limitation is honest and correctly scoped.

No changes requested.

## Review — ready to merge Verified against the live DB rather than the diff alone. **Acceptance claim holds.** `map_railways_12` has 3 870 of 18 294 `fi` rows named (21%), matching the PR body. **The language rule was the one thing worth attacking, and it survives.** `ORDER BY (p.kielikoodi = 'fin') DESC, p.geom <-> r.geom` leads with a non-distance term, so in principle a farther Finnish name could beat a nearer Swedish one belonging to a *different* traffic site. Measured — it never does: ``` picks with a name : 3870 picked farther than pure-nearest : 0 max distance gap : 0.000 ``` 876 rows differ in *text* from a naive nearest pick, and all of them are bilingual rows at identical coordinates. So the fin preference only ever breaks ties at one site, which is exactly the stated intent. No cross-site leakage. **Minor, non-blocking:** leading the `ORDER BY` with the boolean means PostGIS cannot use index-assisted KNN ordering, but `ST_DWithin` already bounds the candidate set, so it does not matter here. The documented `S:T MICHEL` → `S:t Michel` initcap limitation is honest and correctly scoped. No changes requested.
art merged commit cddaeb5955 into main 2026-08-11 07:09:02 +00:00
art deleted branch issue-86-fi-railway-names 2026-08-11 07:09:02 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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!88
No description provided.