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.

Weekly
Pipeline releases
New processing version with rule improvements deployed every week
~7 days
Static GTFS refresh
All 7,000+ feeds fully reprocessed and published on a weekly cycle
15–60 sec
Real-time data latency
GTFS-RT vehicle positions and trip updates ingested live from 1,000+ feeds

Static GTFS processing pipeline

Input
7,000+ Raw GTFS
Transit operator feeds
Stage 0
Acquisition
Download, unpack, normalise
Stage 1
Consolidation
Merge overlapping operators
Intermediate
Unified GTFS
1 feed
Stage 2
Correct, enrich, deduplicate
150+ checks
Output
Improved GTFS
+ GeoJSON & stats

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.

Every name reads like it was written by a human

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.

"oxford STREET !!" → "Oxford Street"
Route names that actually work in a list

Short and long names are automatically balanced against each other - nothing empty, nothing duplicated, nothing so long it breaks your route picker.

short_name empty, long_name "25" → promoted
No repeated information cluttering stop data

When a description just restates the stop name, we clear it instead of shipping the same string twice under two different fields.

stop_desc = stop_name → cleared
Nothing ships nameless

A stop, agency, or trip with no name gets a clear, honest placeholder instead of a blank field that breaks your display logic.

agency_name empty → "Transit Agency"
Riders always see where the vehicle is going

When a provider skips the trip's headsign, we fill it in from the final stop, so your app can always show a destination.

trip_headsign empty → filled from last stop
The right language, every time

When a feed's declared language doesn't match its agency, we correct it - so your localisation logic can actually trust the field.

feed_lang wrong → derived from agency_lang

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.

Calendars that reflect real service, not scheduling debris

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.

2 identical Mon-Fri rows → 1 row
Time travel never makes it into your schedules

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.

arrival 14:05, departure 14:02 → corrected
Every trip has a real departure and arrival time

Blank time fields are validated and filled in, so nothing downstream has to guess when a vehicle actually leaves.

arrival/departure blank → filled
No trips that physically can't run

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.

trip missing edge time → removed
Overnight service handled correctly

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.

27:15:00 → normalised, service shifted
No ambiguity about which times are exact

Timepoint is set automatically from whether real arrival/departure data exists, instead of being left blank for every consumer to interpret differently.

timepoint blank → exact or approximate
Physically impossible trips never reach your users

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.

overlapping headway rows → deduplicated

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.

Every stop knows what kind of stop it is

Boarding-only stops get their location type set automatically, and trips referencing an incompatible stop type are caught before they break your map.

location_type null → 0 (platform)
Stations come with real entrances, not just a dot on a 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.

station, 0 entrances → entrances added
No trip breaks because a stop was never defined

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.

stop referenced, never defined → reconstructed from NAPTAN
Accessibility data you can actually trust

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".

1 of 3 platforms accessible → parent: accessible
Precise without the bloat

Coordinates are rounded to six decimal places - sub-meter accuracy - cutting file size with zero loss of positional precision.

51.5074123456 → 51.507412
One stop, one record - even when two operators both published it

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.

2 stops, 20m apart, same heading → merged
Every stop knows what kind of transit serves it

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.

3 bus routes serve stop → mode: bus
City-level answers, without you doing the geography

Every stop is resolved to its containing city, so city-scoped exports and API filters simply work.

coordinates → city: London
No trips that go nowhere

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.

trip with 1 stop → removed

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.

Wrong vehicle types never reach your users

Incorrect vehicle types, route names, and agency assignments are caught and corrected against a table our data team actively maintains.

route_type miscoded → corrected
Nothing ships as an unrecognisable route type

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.

route_type missing → Bus, flagged for review
Route colors that are always legible

Invalid color codes are corrected, and a missing text color is set based on the background so a route badge is never unreadable.

route_text_color missing → set for contrast
No duplicate links cluttering route data

A route URL that just repeats the agency's own site is cleared rather than shipped as noise.

route_url = agency_url → cleared
Even the small details are covered

Ferry trips get their bike-carry field set correctly, closing a validator warning most feeds ship with by default.

ferry trip, bikes_allowed blank → allowed
Fare data that actually validates

Fare rule fields are checked and corrected against the GTFS spec before they reach you.

invalid fare_rules field → corrected

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.

The same route, recognised everywhere it's published

Routes are fingerprinted from their real geography, so the same route published by two different sources is recognised as one route - not listed twice.

2 sources, same route → 1 canonical record
A trip's identity survives the provider changing its own IDs

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.

provider trip_id changes → our hash stays stable
No duplicate trips inflating your data

Trips sharing a route and schedule fingerprint are recognised as the same trip and merged into one canonical record.

2 trips, identical schedule → merged
Results arrive in the right order, every time

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.

08:15, 08:02, 08:40 → sort order 2, 1, 3
Clean, gap-free stop ordering

Non-standard stop sequence numbering is renumbered from zero with no gaps, so anything that iterates over a trip's stops just works.

sequence 3, 7, 12 → 0, 1, 2
A stop's identity survives every update cycle

Stops are fingerprinted from name, coordinates, and transport type - the basis for recognising the same physical stop across sources and weekly updates.

name + coords + mode → stable stop hash
Route shapes that render fast without looking wrong

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.

4,200 shape points → simplified

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.

Agency contact info you can actually use

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.

malformed agency_email → cleared
Never guessing what language an agency speaks

A missing agency language is filled in from the country's default, so your localisation logic always has something real to work with.

agency_lang empty → country default
Departure times that are correct, not just plausible

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.

"GMT+1" → "Europe/Paris"
Transfers that actually point somewhere real

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.

transfer to unknown stop_id → removed
Instant answers to "what serves this stop"

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.

stop → [25, 73, N25] precomputed

Real-time processing pipeline

Input
1,000+ GTFS-RT
Vehicle positions, trip updates
Stage 1
Parse & Validate
Protobuf decode
Stage 2
Geographic Filter
Discard out-of-region
Stage 3
ID Normalization
Route & stop ID remapping
Stage 4
Trip Matching
Multi-signal matching
Stage 5
Arrival Prediction
Interpolation for gaps
Output
Real-time API
Matched positions & predictions

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.

UK (NAPTAN integration): Enriches stops with names, coordinates, ATCO codes, and platform data from the national NAPTAN database. Adds parent stations and entrances. Normalizes operator names using the National Operator Code (NOC) register.
Ireland: Enriches stops with NAPTAN Ireland data. Adds manually collected train station entrances and exits for stations not covered by official datasets.
Stop ATCO code correction: Fixes incorrect ATCO codes when a provider uses non-standard identifiers, which would otherwise break real-time data matching against vehicle position streams.
Traveline enrichment: Applies regional Traveline reports to geographically delimit overlapping dataset boundaries and improve GTFS-RT match rates for UK bus data.
Custom rules per client: For enterprise customers requiring specific data transformations or output formats, we implement dedicated processing rules scoped to their feed group.

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:

1,500+
Developers building on this data
30M+
API calls served every month
2–99 ms
Typical API response time
99.5%
API uptime SLA
At global scale

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.

Request enhanced data