Skip to content

Add stop_direction to stops.txt — cardinal direction of travel for stop/platform disambiguation #645

Description

@ciospettw

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:

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

  2. 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.bearingnot 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_id,stop_name,stop_lat,stop_lon,location_type,parent_station,stop_direction
S1,Via Roma,41.9010,12.4910,0,,3
S2,Via Roma,41.9011,12.4912,0,,7

Example — with station hierarchy (parent_station present)

stop_id,stop_name,stop_lat,stop_lon,location_type,parent_station,stop_direction
ST,Via Roma,41.9010,12.4911,1,,
S1,Via Roma,41.9010,12.4910,0,ST,3
S2,Via Roma,41.9011,12.4912,0,ST,7

Relationship to existing fields (no conflicts)

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_direction identifies the direction, platform_code identifies 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

  1. 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.
  2. Overload platform_code / stop_name — free text, inconsistent, not localizable, not machine-readable. Rejected (same reasoning the spec already gives for platform_code i18n).
  3. 4-point enum — unstable on diagonal roads (bin-boundary ambiguity). Rejected in favor of 8 points.
  4. 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.
  • Proposed discussion period: at least one month.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions