Skip to content

Commit ff67dad

Browse files
committed
docs(roads): correct the record of what was actually losing records
The page described all of this as a TMC problem. Only Niedersachsen was. Hessen, Schleswig-Holstein and Thüringen were dropping coordinates that sat in the payload under element names nothing checked; Hamburg's were in an XSD wildcard; Mecklenburg-Vorpommern's were a projected grid stored as degrees. Records the per-source reasons and shapes that told them apart, the road cross-check that unlocks another table edition, and the two remainders that are still open.
1 parent ad17868 commit ff67dad

1 file changed

Lines changed: 104 additions & 61 deletions

File tree

docs/tmc-location-tables.md

Lines changed: 104 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,52 @@
1-
# TMC location tables
2-
3-
Some DATEX publishers never send coordinates. They say _where_ by Alert-C
4-
location code — an integer into a national TMC location table — and expect the
5-
consumer to hold that table. Without it such records cannot be placed on a map,
6-
so they were dropped.
7-
8-
That was not a marginal loss. Two German states published nothing usable at all:
9-
10-
| Source | Records per poll | Stored |
11-
| ---------------------------------- | ---------------- | ------- |
12-
| `de-ni-mobilithek` (Niedersachsen) | 1271 | **0** |
13-
| `de-hh-mobilithek` (Hamburg) | 316 | **0** |
14-
| `de-he-mobilithek` (Hessen) | 407 | partial |
15-
16-
Both feeds looked healthy on the status page — they fetched successfully and
17-
reported no errors — which is why the per-source skip counter came first. It
18-
sized the problem; this resolves it.
19-
20-
## The table
1+
# Placing DATEX records that carry no coordinates
2+
3+
Several German publishers were storing nothing, or nothing usable, while their
4+
feeds reported success. The causes turned out to be four different things that
5+
all looked identical from outside: a record went in, no geometry came out.
6+
7+
| Source | Was lost per poll | Cause |
8+
| ---------------------------------- | ----------------- | ------------------------------------- |
9+
| `de-ni-mobilithek` (Niedersachsen) | 1271 → 0 stored | Alert-C codes, table edition 17.0 |
10+
| `de-sh-mobilithek` (Schleswig-H.) | 614 | position only in `locationForDisplay` |
11+
| `de-th-mobilithek` (Thüringen) | 539 | position only in a point extension |
12+
| `de-he-mobilithek` (Hessen) | 407 | position only in `locationForDisplay` |
13+
| `de-hh-mobilithek` (Hamburg) | 316 → 0 stored | `posList` inside an unnamed `any` |
14+
| `de-mv-mobilithek` (Meck.-Vorp.) | (worse) 62 stored | UTM grid stored as if it were degrees |
15+
16+
Only the first was a TMC problem. The rest were coordinates sitting in the
17+
payload that nothing was reading, which is why the per-source skip counter and
18+
its reasons came first: they turned one number into six diagnoses.
19+
20+
## Reading coordinates by content, not by name
21+
22+
Three of these were the same mistake made three times — a coordinate reachable
23+
only through an element whose name we did not happen to check. Allow-listing
24+
names loses to the next publisher, so two content-based fallbacks run when a
25+
record yields no geometry by any named route:
26+
27+
1. **A coordinate list under any element.** A whitespace-separated run of
28+
numbers that reads as a sequence of plausible WGS84 pairs is a coordinate
29+
list, whatever holds it. Hamburg wraps its `posList` in an XSD wildcard
30+
literally named `any`. Two valid pairs are required, so identifiers and
31+
measurements do not qualify.
32+
2. **A lone display position.** An element carrying finite `latitude` and
33+
`longitude` leaves — `locationForDisplay`, point extensions. This is a label
34+
position rather than an extent, so it is tried last.
35+
36+
Neither can override real geometry: both run only when nothing better was found.
37+
38+
## Coordinates that are not coordinates
39+
40+
Mecklenburg-Vorpommern publishes UTM zone 33 with the zone as an easting prefix
41+
(`33342865` = zone 33, easting 342865) and declares no `srsName`, so its grid
42+
values were stored as degrees — a bounding box nowhere on Earth, with no error
43+
anywhere. A feed can now declare the CRS its payload omits (`srsName` on the
44+
descriptor), and a coordinate outside the range degrees can occupy is refused
45+
rather than stored. An undeclared projection is invisible until the numbers
46+
land, where nothing can recover them; dropping the record puts it in the skip
47+
count instead.
48+
49+
## The TMC location table
2150

2251
Germany's Location Code List **22.0** is published by the Bundesanstalt für
2352
Straßenwesen (BASt) under **CC BY 4.0**, as a direct download in the ISO 14819-3
@@ -31,23 +60,18 @@ the vendored snapshot is not expected to go stale.
3160

3261
Every event placed from the table carries `attributes.locationTable`, so the
3362
attribution travels with the data and consumers can tell table-derived geometry
34-
apart from a coordinate the publisher actually sent.
35-
36-
Only tables whose licence permits redistribution are vendored. A feed
37-
referencing a country we hold no table for resolves to `no-table` and its
38-
records stay counted as unmapped — never placed by guesswork.
63+
apart from a coordinate the publisher actually sent. Only tables whose licence
64+
permits redistribution are vendored; a feed referencing a country we hold no
65+
table for resolves to `no-table` and stays counted as unmapped.
3966

40-
## Why the version must match exactly
67+
## Why the edition matters, and how a record can override it
4168

42-
Location codes are renumbered between table editions, so using the wrong edition
43-
does not degrade gracefully: it returns a _confident_ coordinate on the wrong
44-
road. The resolver therefore refuses any record whose declared
45-
`alertCLocationTableVersion` is not the table's own.
46-
47-
This is not a theoretical concern. OpenStreetMap carries German TMC codes as
48-
tags (`TMC:cid_58:tabcd_1:LocationCode`) and was the obvious redistributable
69+
Location codes are renumbered between editions, so using the wrong one does not
70+
degrade gracefully: it returns a _confident_ coordinate on the wrong road.
71+
OpenStreetMap carries German TMC codes as tags
72+
(`TMC:cid_58:tabcd_1:LocationCode`) and was the obvious redistributable
4973
candidate, but its data is largely LCL v8/v9 vintage from around 2010. Scored
50-
against real records that carry both a code and coordinates:
74+
against real records carrying both a code and coordinates:
5175

5276
| Table | Codes present | Median error |
5377
| ------------------------------------ | ------------- | -------------------- |
@@ -56,43 +80,62 @@ against real records that carry both a code and coordinates:
5680
| **Published LCL 22.0** | **~100%** | **128 m, p90 710 m** |
5781

5882
The middle row is the point: the _mechanism_ was never wrong, the _edition_ was.
59-
A version guard is what separates those two outcomes.
6083

61-
## Accuracy, and what it means
84+
Niedersachsen references edition **17.0**, which is not obtainable. Refusing per
85+
edition would lose the publisher entirely, so the table carries each coded
86+
point's road number (96% of them) and a code from another edition is accepted
87+
only when **every point it resolves lands on the road the record itself names**.
88+
Agreement is evidence the code still means the same place; disagreement is
89+
exactly the renumbering the guard exists for, and still refuses. Both ends of a
90+
linear location must agree, and a record naming no road — or a code the table
91+
knows no road for — is refused. Such placements are marked `viaRoadMatch`.
92+
93+
In production this places ~750 Niedersachsen records a poll, within the state's
94+
own bounding box, with 6 refused because the road did not vouch for them.
6295

63-
TMC is a coarse referencing system by design. Consecutive coded points on the
64-
same road are a median 2.5 km apart, so no resolver can place an event more
65-
precisely than the table's own granularity.
96+
## Accuracy, and what it means
6697

67-
Against 1206 records carrying both a code and real coordinates, resolved
68-
geometry lands a **median 128 m** from the record's true extent, 94.4% within
69-
1 km. That is well inside the table's granularity, and it is why a linear
70-
location is resolved as the _stretch between_ its two coded points — following
71-
the table's own point chain — rather than as a single point or a straight chord
72-
between the endpoints.
98+
TMC is coarse by design: consecutive coded points on the same road are a median
99+
2.5 km apart, so no resolver can beat the table's own granularity. Against 1206
100+
records carrying both a code and real coordinates, resolved geometry lands a
101+
**median 128 m** from the record's true extent, 94.4% within 1 km. That is why a
102+
linear location resolves to the _stretch between_ its two coded points, walking
103+
the table's own point chain, rather than to a single point or a straight chord.
73104

74-
Re-check any time with:
105+
Re-check any time — it runs against the real resolver, so it measures shipped
106+
behaviour rather than a model of it:
75107

76108
```sh
77109
pnpm tsx scripts/validate-tmc-table.ts ground-truth.csv
78110
```
79111

80-
The SQL that produces `ground-truth.csv` is documented at the top of that
81-
script. It runs against the real resolver, so it measures shipped behaviour
82-
rather than a model of it.
112+
The SQL that produces `ground-truth.csv` is documented at the top of that script.
113+
114+
## Records that still cannot be placed
115+
116+
Counted per source in `GET /feeds/status`, with the reason logged:
83117

84-
## Unresolved records
118+
| Reason | Meaning |
119+
| ------------------ | ----------------------------------------------------------- |
120+
| `no-alertc` | Locates itself some other way; the shape logged says how |
121+
| `version-mismatch` | Names an edition we do not hold, and no road vouched for it |
122+
| `version-missing` | Names no edition, so there is nothing to check ours against |
123+
| `no-table` | No table held for that country — usually a licensing limit |
124+
| `unknown-code` | The referenced code is absent from the table |
125+
| `no-reference` | The Alert-C block is an empty placeholder (code `0`) |
85126

86-
Records that still cannot be placed are counted per source and surfaced in
87-
`GET /feeds/status`, with the reason logged:
127+
A `no-alertc` record also logs how it describes its location — the location
128+
element's type, its children, and the leaf names beneath. A count says a
129+
publisher is being lost; the shape says what to build to stop losing it. Where
130+
even the leaves are opaque (`any` inside `any`), a short excerpt of the values is
131+
included, which is how Hamburg was diagnosed.
88132

89-
| Reason | Meaning |
90-
| ------------------ | ---------------------------------------------------------- |
91-
| `no-table` | No table held for that country — usually a licensing limit |
92-
| `version-mismatch` | The record references an edition we do not hold |
93-
| `unknown-code` | The referenced code is absent from the table |
94-
| `no-reference` | The Alert-C block is an empty placeholder (code `0`) |
133+
Two known remainders:
95134

96-
The last is common: Brandenburg emits an otherwise-empty Alert-C block on every
97-
record. Treating code `0` as a location would pile every such record onto one
98-
arbitrary point, so it is explicitly rejected.
135+
- **Niedersachsen, ~510 a poll**`linearWithinLinearElement` carrying a road
136+
number and a distance along it. That is linear referencing against the state
137+
road network (NWSIB-NI), not a location table, and needs that dataset.
138+
- **Brandenburg** emits an otherwise-empty Alert-C block with code `0` on every
139+
record. Treating that as a location would pile every such record onto one
140+
arbitrary point, so it is explicitly rejected; those records carry real
141+
coordinates anyway.

0 commit comments

Comments
 (0)