The static join and routeIdSource
The realtime feed almost never carries route_id — 2 in 1,000 vehicles. Joining trip_id against the static GTFS archive takes coverage to 96%, and marks every backfilled value.
GTFS-realtime identifies a trip by trip_id and, optionally, by
route_id. SL's feeds almost never send the route id. Measured across this capture:
| Entity | Route id from the feed | After the static join |
|---|---|---|
| Vehicle positions | 2 of 1,000 (0.2%) | 964 of 1,000 (96.4%) |
| Trip updates | 1 of 934 (0.1%) | 934 of 934 (100%) |
Without the join there is essentially nothing to group or filter by. ?routeId= would
match two vehicles in the entire feed, list routes would be
nearly empty, and every id in every response would stay an opaque sixteen-digit number.
How the join works
At load time the API reads three files from the static GTFS Sweden archive and builds lookup tables:
| File | Kept | Gives |
|---|---|---|
trips.txt | 1,096 | route_id, direction_id and trip_headsign for a
trip id. Filtered to the trip ids the realtime feed actually references — this is a
nationwide file, so keeping all of it would tie memory to the size of Sweden rather than
to the size of the feed.
|
routes.txt | 7,880 | Short name, long name, agency and route type. Small enough to keep whole. |
stops.txt | 181,369 | Stop names, coordinates, location type and parent station — the last of which also builds the station-to-platform map. |
routeIdSource
Enrichment is additive and never silent. When a routeId was backfilled from
trips.txt rather than supplied by the feed, the trip reference is marked:
{
"trip": {
"tripId": "14010000710000247",
"routeId": "9011001087500000",
"routeIdSource": "static",
"directionId": 1,
"startDate": "20260427",
"route": {
"routeId": "9011001087500000",
"agencyId": "505000000000000001",
"shortName": "875",
"type": 700
}
}
} routeIdSource: "static" means “we looked this up”. Its
absence means the feed sent the route id itself — which, in this capture,
happens three times in total. If you are building anything that reasons about data provenance,
that one key is the difference between a reported fact and a joined one.
When there is no match
A trip id absent from the archive is left exactly as the feed reported it. No route
object, no headsign, no invented name — the fields are simply not there, in keeping
with absent is not zero. That is why vehicle coverage
is 96.4% rather than 100%: those trips have no counterpart in the archive, usually because the
static feed and the realtime feed were published at different times.
You can see the join's health directly at /api/feed, under static. If available is false, the archive was not
found: the API still serves realtime data, but unenriched — no names anywhere, and route coverage
back down to under 1%.
{
"available": true,
"dir": "/…/data/gtfs",
"counts": {
"routes": 7880,
"stops": 181369,
"trips": 1096,
"tripsRequested": 1096
},
"loadMs": 1162
} tripsRequested is how many distinct trip ids the realtime feed referenced;
trips is how many of those were found. A gap between them is the unmatched
remainder — worth watching if enrichment starts thinning out.