Closes #69 #75

Merged
art merged 1 commit from issue-69-metro-merge-transitive into main 2026-08-09 04:12:47 +00:00
Owner

Closes #69


Metro entrance merge is transitive — cluster group centroids with DBSCAN

The metro_merged CTE in conf/plmap.yaml collapsed same-station entrance
groups by pairwise nearest-largest winner within 250 m. That relation is not
transitive: a chain of three groups (A–B and B–C under 250 m, A–C over) would
emit two stations — one named B but positioned at A's centroid, one named C
holding B's and C's entrances. A wrong name and a wrong position, worse than
the duplicate the merge exists to prevent.

The fix — replace the self-join with the same primitive the tram and rail
branches already use:

metro_merged AS (
  SELECT nazwa, n,
         first_value(nazwa) OVER (PARTITION BY mc ORDER BY n DESC, nazwa) AS win
  FROM (
    SELECT nazwa, n, ST_ClusterDBSCAN(c, 250, 1) OVER () AS mc
    FROM metro
  ) cl
)

DBSCAN's transitive closure is exactly the "join all same-named points that are
near each other" semantics the recipe comment documents. minpoints 1 keeps every
group (singletons included), and the tie-break nazwa keeps first_value
deterministic. One fewer bespoke pattern in the file.

It does not fire on the current data — the issue is latent (39 groups → 38
stations, chain_broken 0) — but the recipe is documented as general, so the next
named-entrance source inherits the fix instead of the bug.

Verification (live DB, then a -layer stations reimport)

  • Identical output on current data: 39 entrance groups → 38 stations before
    and after, with identical name sets — all 39 group winners agree pairwise.
  • Wileński still merges: both groups land in DBSCAN cluster 0; the live
    point is the centroid of all 13 entrances (distance 0.000).
  • The 17 of 38 centroids that differ from the old query differ only in last-ulp
    float noise (ST_Distance = 0.0000 in EPSG:3857) — GEOS collection scan
    order, not a position change.
  • pl stations partitions: z9 0 (Poland has no importance-3 rung — the
    documented empty rung), z10/z11 3225, z12–z14 4052; no NULL names at z14
    (metro 38 / tram 827 / rail 3187).
  • Decoded z11 Warsaw tile (scripts/mvt-decode.py): 54 station features,
    27 metro, Wileński present.
  • Real-browser CDP check: z12 Warsaw renders 27 metro icons; the two
    console 404s are pre-existing (favicon, an ungenerated italic glyph) and
    unrelated to this change.
  • go test ./... green in importer/ and mapsrv/.

Notes for the reviewer

  • The GTFS feeds refreshed during the import (server copies changed, fetched
    through the declared-downloads path) — the tram counts reflect current feeds,
    a normal reimport side effect. Metro/rail come from BDOT10k, unchanged.
  • conf/plmap.yaml comments and docs/country-poland.md updated to describe
    the DBSCAN mechanism in place of the old "spatial join at 250 m".
Closes #69 --- ## Metro entrance merge is transitive — cluster group centroids with DBSCAN The `metro_merged` CTE in `conf/plmap.yaml` collapsed same-station entrance groups by **pairwise nearest-largest** winner within 250 m. That relation is not transitive: a chain of three groups (A–B and B–C under 250 m, A–C over) would emit two stations — one named B but positioned at A's centroid, one named C holding B's and C's entrances. A wrong name *and* a wrong position, worse than the duplicate the merge exists to prevent. **The fix** — replace the self-join with the same primitive the tram and rail branches already use: ```sql metro_merged AS ( SELECT nazwa, n, first_value(nazwa) OVER (PARTITION BY mc ORDER BY n DESC, nazwa) AS win FROM ( SELECT nazwa, n, ST_ClusterDBSCAN(c, 250, 1) OVER () AS mc FROM metro ) cl ) ``` DBSCAN's transitive closure is exactly the "join all same-named points that are near each other" semantics the recipe comment documents. minpoints 1 keeps every group (singletons included), and the tie-break `nazwa` keeps `first_value` deterministic. One fewer bespoke pattern in the file. It does **not** fire on the current data — the issue is latent (39 groups → 38 stations, chain_broken 0) — but the recipe is documented as general, so the next named-entrance source inherits the fix instead of the bug. ## Verification (live DB, then a `-layer stations` reimport) - **Identical output on current data**: 39 entrance groups → 38 stations before and after, with identical name sets — all 39 group winners agree pairwise. - **Wileński still merges**: both groups land in DBSCAN cluster 0; the live point is the centroid of all 13 entrances (distance 0.000). - The 17 of 38 centroids that differ from the old query differ only in last-ulp float noise (`ST_Distance` = 0.0000 in EPSG:3857) — GEOS collection scan order, not a position change. - **pl stations partitions**: z9 0 (Poland has no importance-3 rung — the documented empty rung), z10/z11 3225, z12–z14 4052; no NULL names at z14 (metro 38 / tram 827 / rail 3187). - **Decoded z11 Warsaw tile** (`scripts/mvt-decode.py`): 54 station features, 27 metro, Wileński present. - **Real-browser CDP check**: z12 Warsaw renders 27 metro icons; the two console 404s are pre-existing (favicon, an ungenerated italic glyph) and unrelated to this change. - `go test ./...` green in `importer/` and `mapsrv/`. ## Notes for the reviewer - The GTFS feeds refreshed during the import (server copies changed, fetched through the declared-downloads path) — the tram counts reflect current feeds, a normal reimport side effect. Metro/rail come from BDOT10k, unchanged. - `conf/plmap.yaml` comments and `docs/country-poland.md` updated to describe the DBSCAN mechanism in place of the old "spatial join at 250 m".
The metro_merged CTE in conf/plmap.yaml collapsed same-station entrance groups
by pairwise nearest-largest winner within 250 m. That relation is not
transitive, so a chain of three groups (A–B and B–C within 250 m, A–C not)
would emit two stations: one named B but positioned at A's centroid, and one
named C holding B's and C's entrances — a wrong name and a wrong position,
worse than the duplicate the merge exists to prevent.

Replace the pairwise self-join with ST_ClusterDBSCAN(c, 250, 1) OVER () over
the group centroids — the same primitive the tram and rail branches already
use — and take the modal name per cluster with first_value(nazwa) OVER
(PARTITION BY mc ORDER BY n DESC, nazwa). DBSCAN's transitive closure is
exactly the "join all same-named points that are near each other" semantics
the recipe comment documents.

Not currently firing: Warsaw's 38 stations are correct today, and this run
keeps them so. But the entrance-collapse recipe is documented as general, so
any future named-entrance source (a second Polish metro, say) would have
inherited the chain bug.

Verification against the live pl_oikm, then a -layer stations reimport:
- 39 entrance groups → 38 stations before and after, with identical name
  sets — all 39 group winners agree pairwise.
- Wileński still absorbs Dworzec Wileński (both in DBSCAN cluster 0) and the
  live point is the centroid of all 13 entrances, distance 0.
- 17 of 38 centroids differ from the old query only in last-ulp float noise
  (ST_Distance 0.0000 in EPSG:3857) — GEOS collection scan order, not a
  position change.
- pl stations partitions: z9 0 (no importance-3 rung for Poland, as
  documented), z10/z11 3225, z12–z14 4052; no NULL names at z14
  (metro 38 / tram 827 / rail 3187).
- Decoded z11 Warsaw tile (scripts/mvt-decode.py): 54 station features,
  27 metro, Wileński present. Real-browser CDP check renders 27 metro icons
  at z12; the two console 404s are pre-existing (favicon, an ungenerated
  italic glyph) and unrelated.
- go test ./... green in importer/ and mapsrv/.
art merged commit 77a5fa6c61 into main 2026-08-09 04:12:47 +00:00
art deleted branch issue-69-metro-merge-transitive 2026-08-09 04:12:47 +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!75
No description provided.