Transit data is broken by default.
This is how we fix it.
Any transit agency can publish a GTFS feed, and nobody checks it before it goes live. What actually ships is inconsistent, incomplete, and sometimes simply wrong: duplicated stops, broken calendars, invalid coordinates, arrival times recorded after departure times. A trip planner built directly on top of it sends someone to the wrong corner; an AI agent that repeats a stale timetable strands a human at a bus stop who trusted it. We process 7,000+ feeds from transit operators worldwide through a pipeline built to close that gap - acquiring and normalising every feed, consolidating overlapping operators into one coherent dataset, then running 150+ correction, enrichment, and validation checks against it, refined against real production data. What follows shows why that is necessary, and what it actually finds and fixes.
Actively maintained, not a one-time fix
This pipeline is under continuous development — new rules, improved algorithms, and data quality fixes are shipped every week. Search, schedule, and API responses run on the latest validated pipeline version, released through a blue-green server rotation: traffic moves to a freshly deployed environment instead of restarting one in place. Routing runs on its own dedicated infrastructure with an independent release cycle. The GTFS Downloads API returns original source data only. Submit the data request form to discuss enhanced exports.
Static GTFS processing pipeline
Acquisition and consolidation are where the hardest problems live: DST-aware calendar splitting across timezones, spatial stop deduplication with mode-specific distance thresholds, translation inheritance across sources. That part of the pipeline is deliberately not published step by step - it is the part of this that is hardest to replicate.
What we fix, straight from the pipeline
GTFS is an open, self-published format: any agency can publish a feed, and no central authority validates it before it goes live. We track 10,000+ transit agencies across 90+ countries - from metro systems with dedicated data teams to single-operator bus companies exporting from scheduling software last updated a decade ago. 150+ automated checks run on every feed to catch what that produces. They do not just delete bad data - they clean names into consistent casing, rebuild broken relationships, and give every stop, route, and trip a stable identity and a stable sort order across weekly updates. That is also why the API built on this data is fast: results already arrive named, deduplicated, and correctly ordered, instead of being cleaned up client-side on every request. Here are the ones most worth showing, grouped by what they touch - not the complete list:
Names & identifiers
Every field meant to be a human-readable name ships from the source with empty values, mismatched casing, or two fields silently duplicating each other.
No more ALL-CAPS STOPS or garbled duplicate characters - every name field is cleaned and cased consistently, so your UI never has to hide a rough edge from the source.
Short and long names are automatically balanced against each other - nothing empty, nothing duplicated, nothing so long it breaks your route picker.
When a description just restates the stop name, we clear it instead of shipping the same string twice under two different fields.
A stop, agency, or trip with no name gets a clear, honest placeholder instead of a blank field that breaks your display logic.
When a provider skips the trip's headsign, we fill it in from the final stop, so your app can always show a destination.
When a feed's declared language doesn't match its agency, we correct it - so your localisation logic can actually trust the field.
Calendars & schedules
Service periods that almost nothing runs on, arrival times that land after departure, schedules implying a bus travels faster than physically possible - a scheduling export tool's bugs read exactly like real service to anyone who trusts the raw feed.
Phantom service periods, contradictory calendar rows, and duplicate calendars are caught and removed before they can tell a rider a bus runs on a day it doesn't.
When a stop's arrival lands after its departure at the previous stop - a real, recurring export bug - we correct it, and flag the rare feed where it's too widespread to fix blind.
Blank time fields are validated and filled in, so nothing downstream has to guess when a vehicle actually leaves.
A trip missing its first or last timestamp can't be scheduled - we remove it rather than let it silently break a rider's itinerary.
Late-night departures past midnight are converted into valid GTFS time format without touching the actual schedule - a detail that trips up most naive parsers.
Timepoint is set automatically from whether real arrival/departure data exists, instead of being left blank for every consumer to interpret differently.
Overlapping frequency intervals are cleaned up, and trips implying speeds no vehicle could hit are removed - a rider should never be told a bus travels at 2,000 km/h.
Stops & coordinates
Two operators serving the same corner each publish their own version of the stop, a trip can reference a stop_id the feed itself never got around to defining, and accessibility data is either missing or simply wrong.
Boarding-only stops get their location type set automatically, and trips referencing an incompatible stop type are caught before they break your map.
Entrances, exits, and parent-station links are added from a maintained hub database, and misplaced parent stations get relocated to where they actually are.
When a trip references a stop that the feed's own stops.txt never got around to defining, we reconstruct it - name and coordinates included from official UK NAPTAN data where available - instead of letting the trip silently break.
A station is marked accessible the moment one platform is, and inaccessible only when every platform explicitly is - stops with no data default to unknown, never to a false "no".
Coordinates are rounded to six decimal places - sub-meter accuracy - cutting file size with zero loss of positional precision.
Stops around 20 meters apart and heading the same direction are recognised as the same physical place and merged, instead of showing up twice on your map.
A stop's mode is derived from the routes that actually serve it, so your map and filters render the right icon without extra lookups.
Every stop is resolved to its containing city, so city-scoped exports and API filters simply work.
A trip with a single stop isn't a real transit service - it's removed along with the duplicate stop records that usually come with it.
Routes & fares
A route's vehicle type, color, or fare fields are frequently just wrong - miscoded, blank, or copied from a field that already exists somewhere else in the feed.
Incorrect vehicle types, route names, and agency assignments are caught and corrected against a table our data team actively maintains.
A missing or invalid route type defaults safely to Bus so your app never chokes on an unknown value - and every correction triggers an internal alert, so a person checks the unusual ones.
Invalid color codes are corrected, and a missing text color is set based on the background so a route badge is never unreadable.
A route URL that just repeats the agency's own site is cleared rather than shipped as noise.
Ferry trips get their bike-carry field set correctly, closing a validator warning most feeds ship with by default.
Fare rule fields are checked and corrected against the GTFS spec before they reach you.
Stable identity, ordering & speed
A provider renumbers its own trip IDs between updates, two sources publish the same route under different identifiers, and nothing in the raw feed says what order a day's trips should render in.
Routes are fingerprinted from their real geography, so the same route published by two different sources is recognised as one route - not listed twice.
Trips are fingerprinted from their stop sequence and timing, so the same scheduled service keeps a stable identity across weekly updates - even when the source renumbers its own trip IDs.
Trips sharing a route and schedule fingerprint are recognised as the same trip and merged into one canonical record.
Every trip gets a stable daily sort order computed once during processing - so your app, and ours, never have to sort trips client-side on every request.
Non-standard stop sequence numbering is renumbered from zero with no gaps, so anything that iterates over a trip's stops just works.
Stops are fingerprinted from name, coordinates, and transport type - the basis for recognising the same physical stop across sources and weekly updates.
Unnecessary shape points are stripped and distances recalculated, cutting payload size - and a shape that drifts far from where a trip actually goes is detached rather than left misleading your map.
Agency, timezone & lookups
An agency declares the wrong timezone, or none at all, and every trip planner built on the feed shows arrivals an hour early or late - for every rider, every day, until someone happens to notice.
Malformed emails and URLs are cleared rather than shipped broken, and a missing agency URL gets a placeholder so nothing downstream breaks on a required field.
A missing agency language is filled in from the country's default, so your localisation logic always has something real to work with.
Invalid or geographically mismatched timezones on agencies and stops are corrected - one of the most common causes of a trip planner showing a bus arriving an hour off.
A transfer rule referencing a stop or route that doesn't exist in the feed is removed rather than left to break a connection search.
Every stop gets a precomputed index of the routes that serve it, so your API call for "routes at this stop" is a lookup, not a live join.
Real-time processing pipeline
GTFS-RT synchronization is one of the hardest problems in transit data engineering. Every provider uses its own internal IDs, formats, and naming conventions — which must be matched against the static schedule in real time. Our pipeline includes dedicated real-time matching rules on top of the static processing:
- Geographic sanity check on vehicle positions - coordinates outside the service region are automatically discarded (real-world example: a London feed reporting buses in the US)
- Merging feeds from different operators can produce the same route ID twice - when it does, match priority across the duplicates is preserved explicitly, not picked arbitrarily same operator+line, 2 sources → match priority preserved
- Trip matching isn't a nearest-time guess: when a real-time arrival falls between two scheduled trips, it's assigned using an asymmetric time window, not the midpoint time between 2 scheduled trips → matched via 75/25 window
- A tiered fallback tries the most specific match first and only loosens the criteria when that fails, instead of one all-or-nothing rule exact stop+time unavailable → falls back to route+journey, then route+time
- When a signal genuinely can't be resolved - direction missing, no fallback applies - the match fails closed and returns unmatched rather than guessing direction missing, no fallback resolves → left unmatched
- Arrival time prediction for stops where real-time data is delayed or missing
Country-specific processing
In addition to the universal pipeline, we develop custom processing rules for individual countries that account for local data formats, coding standards, and operator-specific quirks. Country rules run as an additional layer on top of the base pipeline and are tailored to each provider's actual data quality issues.
Not a research pipeline - what runs busmaps.com itself
Every route search, every departure board, and every AI agent query on busmaps.com runs on the exact output of the pipeline above - already named, deduplicated, and sorted, not cleaned up client-side. Licensing the data means licensing what already serves:
The same discipline runs the routing engine behind every trip search: 13 production regions, each rebuilt on a rolling cycle, with stop deduplication that matches on content hashes instead of guessing by distance - rewriting stop references in 500,000-row chunks so a rebuild never locks the database against real traffic. When a single region outgrows 7 GB of raw GTFS, we restart the database mid-export rather than let it choke - the kind of problem you only run into at real scale.
Request clean, production-ready GTFS data
Enhanced GTFS exports, processed validation reports, GeoJSON, country-wide coverage, and custom outputs are available separately from API subscriptions. Tell us which feeds, formats, update frequency, and licensing rights your system needs, and we will review the delivery with you.
Building an AI agent instead? Connect it directly to the MCP server and it can query this same corrected data without an export step.