You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In the vast majority of transit networks, a single on-street location is served by two separate stops on opposite sides of the road — one for each direction of travel (north/south or east/west). GTFS today has no machine-readable, geographic way to express which direction of travel a stop serves.
This creates two concrete, rider-facing problems:
Riders cannot reliably tell the two stops apart. When an app shows two stops a few meters apart, or a rider physically stands between two poles, there is no structured signal telling them (or the app) which pole is "the one going my way." Today this information, when present at all, is buried in free text inside stop_name (e.g. "Main St (EB)") — inconsistent, language-locked, and not parseable.
Consumers cannot reliably snap a realtime vehicle to the correct stop of a pair. A GTFS-Realtime VehiclePosition carries a bearing, but the static stop has no comparable attribute. Matching a moving vehicle to one of two opposite stops by proximity alone is a ~50/50 guess, which degrades the arrival predictions shown to riders.
The direction of travel served by a stop is stable, intrinsic to the physical stop, and known to the agency (it is, in practice, printed on the pole). It is, however, expensive and unreliable for each consumer to re-derive: it requires high-quality shapes.txt (which many feeds lack or provide with poor geometry), per-stop geometric computation, and still produces inconsistent results at bin boundaries. Standardizing it lets the producer publish the authoritative value once, for all consumers.
Important clarification (and why this works on curves): the value describes the direction the vehicle is heading as it serves / approaches that specific stop (its instantaneous heading at the stop), not where the trip eventually ends up. On a curved road, only the local heading at the stop matters — if a vehicle passes the stop heading west, the stop is "West", regardless of whether the line later turns south. This keeps the attribute well-defined for every stop, including those on bends, ramps and curves.
Use cases
1. Stop disambiguation in the UI (primary, rider-facing).
A rider opens the app at a junction served by two opposite poles. Instead of two identical "Main St" entries, the app labels them "Main St — Northbound" and "Main St — Southbound" and walks the rider to the correct pole. The enum is a language-neutral code that each app localizes (Nord / North / Norte), the same internationalization rationale already used for platform_code.
2. More accurate realtime, hence better predictions (rider-facing, via consumer logic).
A consumer matching a GTFS-Realtime vehicle to the network compares the vehicle's position.bearing against the candidate stops' stop_direction and snaps to the correct stop of the pair. The result — a correct, non-flickering arrival countdown for the right pole — is passenger information.
3. Automatic detour / re-route detection (producer & consumer).
When a vehicle deviates from its scheduled shape, systems that re-attach it to nearby stops need each stop's served direction to avoid attaching the vehicle to the opposite-direction pole.
Proposed solution
Add one optional field, stop_direction, to stops.txt:
Field
Type
Presence
Description
stop_direction
Enum
Optional
Cardinal direction of travel of vehicles as they serve / approach this stop/platform, quantized to the 8 principal compass points. This is the instantaneous heading of the vehicle while serving the stop — semantically aligned with GTFS-Realtime VehiclePosition.position.bearing — not the side of the street, the orientation of the stop pole, nor the trip's final destination. Because only the local heading at the stop is used, the value is well-defined even on curves. Valid options:
0 (or empty) – Not specified, or variable (terminals, loops, or a single stop served in multiple directions). 1 – North (bearing 337.5°–22.5°) 2 – Northeast (22.5°–67.5°) 3 – East (67.5°–112.5°) 4 – Southeast (112.5°–157.5°) 5 – South (157.5°–202.5°) 6 – Southwest (202.5°–247.5°) 7 – West (247.5°–292.5°) 8 – Northwest (292.5°–337.5°)
Conditionally Forbidden: - Forbidden for stations (location_type=1), entrances/exits (location_type=2) and generic nodes (location_type=3). - Optional for stops/platforms (location_type=0) and boarding areas (location_type=4).
Why 8 points, not 4 or 360.
360° (continuous): rejected for rider display — people understand "North/South", not "187° vs 7°". (A degree value also lives more naturally in GTFS-Realtime, which already has bearing.)
4 points (N/E/S/W): rejected for robustness — a road running NE–SW puts both directions near a bin boundary (~45° / ~225°), so different producers/algorithms classify the same stop into different bins. 8 points place opposite stops a stable 4 bins apart and remain fully human-readable ("Northeast").
Example — independent stops (typical bus case, no parent_station)
stop_direction is orthogonal to every existing field; the apparent overlaps answer different questions and compose with the new field.
parent_station / location_type — These express physical hierarchy ("these poles belong to one station"), not direction. They do not solve the problem: the common bus case models the two opposite poles as two independent location_type=0 stops with no parent_station at all, so nothing today indicates which serves which way. stop_direction is therefore designed to work standalone (no parent_station required). When a hierarchy is present, the two children naturally carry distinct stop_direction values — parent_station groups, stop_direction orients. The new field is forbidden on location_type 1/2/3, which have no travel direction.
platform_code — A free-text, opaque identifier of a platform ("G", "3"). Some agencies overload it with "EB"/"WB", but it is not machine-interpretable or geographic. stop_directionidentifies the direction, platform_codeidentifies the platform; they coexist on the same record.
trips.direction_id — Binary (0/1), per-trip, agency-defined, explicitly "should not be used in routing" and carrying no geographic meaning. stop_direction is geographic, per-stop, 8-valued. Complementary, different level.
stop_headsign / trip_headsign — The rider-facing destination ("to Termini"), i.e. where the trip is going, not which way it points here. The two pair well in a UI ("to Termini · heading North").
stop_name / tts_stop_name — Lets agencies stop encoding direction in the name as free text, or keep both consistently.
shapes.txt (relationship, not conflict) — The derivation source. stop_direction is the static, quantized, authoritative counterpart (the agency knows ground truth; many feeds have no shapes). A validator may warn if stop_direction disagrees with shape geometry where shapes exist.
GTFS-Realtime VehiclePosition.position.bearing (relationship, not conflict) — The same physical quantity, realtime and continuous. Aligning the static field's semantics to it is intentional and is what makes use case Describe CHANGES process for GTFS-RT Spec. #2 possible.
No interaction at all with zone_id, level_id, pathways.txt, levels.txt, transfers.txt, Fares v2 areas.
Edge cases (handled by value 0)
Terminals, loops, turnarounds — vehicles depart in differing directions from one pole → 0.
A single stop served in both directions (rare) → 0.
Stop not split into a pair — feeds that don't model double stops simply leave it 0/empty; nothing breaks.
Backwards compatibility
Fully backwards-compatible: a new optional column. Existing feeds remain valid; existing parsers ignore the column. Empty/absent ⇒ 0 (not specified).
Alternatives considered
Continuous bearing (0–359°) in stops.txt — better for machine matching but poor for rider display and partly redundant with GTFS-Realtime bearing. Rejected in favor of a human-meaningful enum; the 8-point bins are defined in degree ranges so the machine-matching use case is still fully served.
Overload platform_code / stop_name — free text, inconsistent, not localizable, not machine-readable. Rejected (same reasoning the spec already gives for platform_code i18n).
4-point enum — unstable on diagonal roads (bin-boundary ambiguity). Rejected in favor of 8 points.
New file — over-engineering; direction is an intrinsic 1:1 attribute of the stop, so it belongs in stops.txt.
Additional information
Prototype / testing plan: the field can be prototyped immediately as an extra column (ignored by official parsers, per the Guiding Principles). It is being prototyped on a realtime backend acting as a consumer (for stop disambiguation and realtime-to-stop matching); a producer willing to populate stop_direction on a real feed would be needed to complete the testing phase.
Prior art: issue "Direction names" in GTFS #228 ("Direction names") explored direction at the trip/destination (semantic) level and stalled on where the data belongs. This proposal is deliberately narrower and at a different level — a geometric, per-stop attribute — so it sidesteps that ambiguity.
Describe the problem
In the vast majority of transit networks, a single on-street location is served by two separate stops on opposite sides of the road — one for each direction of travel (north/south or east/west). GTFS today has no machine-readable, geographic way to express which direction of travel a stop serves.
This creates two concrete, rider-facing problems:
Riders cannot reliably tell the two stops apart. When an app shows two stops a few meters apart, or a rider physically stands between two poles, there is no structured signal telling them (or the app) which pole is "the one going my way." Today this information, when present at all, is buried in free text inside
stop_name(e.g. "Main St (EB)") — inconsistent, language-locked, and not parseable.Consumers cannot reliably snap a realtime vehicle to the correct stop of a pair. A GTFS-Realtime
VehiclePositioncarries abearing, but the static stop has no comparable attribute. Matching a moving vehicle to one of two opposite stops by proximity alone is a ~50/50 guess, which degrades the arrival predictions shown to riders.The direction of travel served by a stop is stable, intrinsic to the physical stop, and known to the agency (it is, in practice, printed on the pole). It is, however, expensive and unreliable for each consumer to re-derive: it requires high-quality
shapes.txt(which many feeds lack or provide with poor geometry), per-stop geometric computation, and still produces inconsistent results at bin boundaries. Standardizing it lets the producer publish the authoritative value once, for all consumers.Use cases
1. Stop disambiguation in the UI (primary, rider-facing).
A rider opens the app at a junction served by two opposite poles. Instead of two identical "Main St" entries, the app labels them "Main St — Northbound" and "Main St — Southbound" and walks the rider to the correct pole. The enum is a language-neutral code that each app localizes (
Nord/North/Norte), the same internationalization rationale already used forplatform_code.2. More accurate realtime, hence better predictions (rider-facing, via consumer logic).
A consumer matching a GTFS-Realtime vehicle to the network compares the vehicle's
position.bearingagainst the candidate stops'stop_directionand snaps to the correct stop of the pair. The result — a correct, non-flickering arrival countdown for the right pole — is passenger information.3. Automatic detour / re-route detection (producer & consumer).
When a vehicle deviates from its scheduled shape, systems that re-attach it to nearby stops need each stop's served direction to avoid attaching the vehicle to the opposite-direction pole.
Proposed solution
Add one optional field,
stop_direction, to stops.txt:stop_directionVehiclePosition.position.bearing— not the side of the street, the orientation of the stop pole, nor the trip's final destination. Because only the local heading at the stop is used, the value is well-defined even on curves. Valid options:0(or empty) – Not specified, or variable (terminals, loops, or a single stop served in multiple directions).1– North (bearing 337.5°–22.5°)2– Northeast (22.5°–67.5°)3– East (67.5°–112.5°)4– Southeast (112.5°–157.5°)5– South (157.5°–202.5°)6– Southwest (202.5°–247.5°)7– West (247.5°–292.5°)8– Northwest (292.5°–337.5°)Conditionally Forbidden:
- Forbidden for stations (
location_type=1), entrances/exits (location_type=2) and generic nodes (location_type=3).- Optional for stops/platforms (
location_type=0) and boarding areas (location_type=4).Why 8 points, not 4 or 360.
bearing.)Example — independent stops (typical bus case, no
parent_station)Example — with station hierarchy (
parent_stationpresent)Relationship to existing fields (no conflicts)
stop_directionis orthogonal to every existing field; the apparent overlaps answer different questions and compose with the new field.parent_station/location_type— These express physical hierarchy ("these poles belong to one station"), not direction. They do not solve the problem: the common bus case models the two opposite poles as two independentlocation_type=0stops with noparent_stationat all, so nothing today indicates which serves which way.stop_directionis therefore designed to work standalone (noparent_stationrequired). When a hierarchy is present, the two children naturally carry distinctstop_directionvalues —parent_stationgroups,stop_directionorients. The new field is forbidden onlocation_type1/2/3, which have no travel direction.platform_code— A free-text, opaque identifier of a platform ("G", "3"). Some agencies overload it with "EB"/"WB", but it is not machine-interpretable or geographic.stop_directionidentifies the direction,platform_codeidentifies the platform; they coexist on the same record.trips.direction_id— Binary (0/1), per-trip, agency-defined, explicitly "should not be used in routing" and carrying no geographic meaning.stop_directionis geographic, per-stop, 8-valued. Complementary, different level.stop_headsign/trip_headsign— The rider-facing destination ("to Termini"), i.e. where the trip is going, not which way it points here. The two pair well in a UI ("to Termini · heading North").stop_name/tts_stop_name— Lets agencies stop encoding direction in the name as free text, or keep both consistently.shapes.txt(relationship, not conflict) — The derivation source.stop_directionis the static, quantized, authoritative counterpart (the agency knows ground truth; many feeds have no shapes). A validator may warn ifstop_directiondisagrees with shape geometry where shapes exist.GTFS-Realtime
VehiclePosition.position.bearing(relationship, not conflict) — The same physical quantity, realtime and continuous. Aligning the static field's semantics to it is intentional and is what makes use case Describe CHANGES process for GTFS-RT Spec. #2 possible.No interaction at all with
zone_id,level_id,pathways.txt,levels.txt,transfers.txt, Fares v2 areas.Edge cases (handled by value
0)0.0.0/empty; nothing breaks.Backwards compatibility
Fully backwards-compatible: a new optional column. Existing feeds remain valid; existing parsers ignore the column. Empty/absent ⇒
0(not specified).Alternatives considered
bearing. Rejected in favor of a human-meaningful enum; the 8-point bins are defined in degree ranges so the machine-matching use case is still fully served.platform_code/stop_name— free text, inconsistent, not localizable, not machine-readable. Rejected (same reasoning the spec already gives forplatform_codei18n).Additional information
stop_directionon a real feed would be needed to complete the testing phase.