Getting there is only half the trip.
The MTA publishes which stations are wheelchair accessible. It does not publish which direction you can leave from. This project resolves ADA accessibility down to the individual platform and can incorporate MTA elevator outage records. The public planner requests official outage records when it opens; trip times and the stop set remain demonstration data.
The problem
A station marked “accessible” can still strand you.
Accessibility is usually published per station — one flag, one answer. But an elevator serves a platform, not a building. Eight stations in the system have an accessible route in exactly one direction, which means a rider can arrive on the accessible platform and then find no accessible way to board the train home.
Those eight platforms are not an edge case. Because a trip inherits the risk of every stop it calls at, they place a return-trip hazard on 22,937 of the system's 77,236 scheduled trips.
How it works
Two public feeds, joined on identifiers that already match.
No name matching anywhere. The static ADA baseline joins to GTFS parent stations 496 of 496; the elevator equipment feed joins 193 of 193.
Read the static ADA baseline
The MTA Subway Stations dataset carries ada_northbound
and ada_southbound per station — the per-direction
truth that a single accessibility flag discards.
Apply MTA elevator outage records
The Elevator & Escalator API supplies current and scheduled outages. Two rules keep it honest: an escalator outage never downgrades a station, because an escalator is not an accessible route; and a redundant elevator going down never downgrades it either, because a parallel unit covers the same route.
Write both encodings into the feed
Directional platforms carry the GTFS rating for their own direction — which is how partial accessibility is expressed without inventing a value the spec does not have. The MTA's own status is preserved verbatim alongside it.
Trace every trip's return leg
Trips in one direction call only at N platforms and
the other only at S, so the return leg for any stop is
unambiguously that station's opposite platform. Each trip is annotated
with the hazards along its route.
| Value | GTFS wheelchair_boarding |
MTA mta_ada_status |
|---|---|---|
0 | No information | Not accessible |
1 | Accessible path exists | Fully accessible |
2 | Boarding not possible | Partially accessible |
Writing the MTA's meaning into the GTFS column would tell every standard trip planner the exact opposite of what was intended — a partially accessible station would read as categorically unusable.
So they are kept apart. Consumers read wheelchair_boarding and get
correct GTFS semantics; mta_ada_status is a non-standard column that
spec-compliant readers ignore.
The design rule
Accessibility warns. It never blocks.
A trip through a platform that is not accessible is still returned, carrying its advisories. Riders have workarounds the feed cannot see — a companion, a transfer, a bus leg, a different exit — so the data's job is to inform the decision, not make it.
step_freereturn_warningoutbound_warningStrict filtering exists, and it is opt-in for a reason. Times Sq to Union Sq returns 10,578 trips by default and zero with ADA accessibility required — which presents to a rider as “no service” rather than “here are your options, with caveats”.
Scope
What this is, and what it is not.
Three pieces: a data layer that produces an augmented GTFS feed, a JSON API that serves it, and an accessible web app built for the riders who need it.
Built Working
- Directional accessibility resolver — per-platform ADA accessibility across all 496 stations
- MTA outage integration — elevator and escalator feeds, with blocking and degraded distinguished
- Return-trip risk detection — every trip annotated with the one-way traps along its route
- Augmented GTFS export — a drop-in feed any standard router can consume
- JSON API — FastAPI over an in-memory feed; trip queries in ~0.1s
- Web app — React, screen-reader first, with trip planning, transfers, equipment detail, and a clearly labeled static demonstration
- Transfers — representative journeys with one change and explicit in-station uncertainty
Next Planned
- Realtime arrivals — the GTFS-RT feeds are reachable and mapped, not yet wired in
- Scheduled rebuilds — the feed is a snapshot; outages change hourly
- Station entrance detail — which specific entrance has the elevator
Out of scope Deliberate
- Deciding for the rider — the data advises; it does not hide options
- Name matching — every join is on a published identifier or it does not ship
- Direction-mapped outages — the feed does not map an elevator to a direction, so a blocking outage conservatively takes out both
- LIRR and Metro-North trip planning — subway routing remains the core planning scope
# Build the augmented feed
python3 gtfs_accessibility.py → ~/Downloads/gtfs_accessible.zip
# Serve it
uvicorn api.main:app --host 0.0.0.0 --port 8000
# Run the app
cd web && npm run dev
Case study
Every interesting choice here was a trade-off, not a feature.
The engineering was mostly joins and lookups. What took the time was deciding what the data was allowed to say — and, more often, what it was not allowed to decide on a rider's behalf.
Where to put a value that means two different things
GTFS and the MTA both use 2, and they disagree.
In GTFS it means boarding is impossible. To the MTA it means partially
accessible. One column cannot hold both.
Write both, separately. Consumers read the GTFS column and get correct GTFS semantics; the MTA's value is preserved verbatim in a non-standard column that spec-compliant readers ignore. Collapsing them would have told every trip planner that a usable station was unusable.
Whether to express something the spec has no word for
GTFS has no value for "accessible in one direction." The obvious move is to invent one, which no other consumer would understand.
Don't extend the spec — use what it already has. The feed carries directional child platforms, so each one takes the rating for its own direction. Partial accessibility falls out of standard GTFS, and any router doing an ordinary stop lookup gets it right without knowing this project exists.
Whether to hide trips a rider probably cannot take
Filtering to accessible trips only is the obvious product decision, and it is what a strict reading of the data supports.
Warn, never block. Riders have workarounds the feed cannot see — a companion, a transfer, a bus leg, a different exit. Times Sq to Union Sq returns 10,578 trips by default and zero under strict filtering, and "no service" is a worse answer than "here are your options, with caveats."
What to do when the data cannot answer the question
The elevator feed does not say which direction of travel an elevator serves. Guessing would produce a more precise-looking answer most of the time.
Be conservative and say so. A blocking outage takes out both directions at that station. It overstates the damage, but the failure mode is a rider taking a longer route — not a rider stranded on a platform they cannot leave.
How much to trust a name
Three datasets, three naming conventions, and station complexes that appear under several names at once. Fuzzy matching would have joined them quickly.
Join on published identifiers or do not ship. Both sources key to GTFS parent stations exactly — 496 of 496 for the ADA baseline, 193 of 193 for equipment. A fuzzy match that is 98% right is a station that silently lies to someone.
What the data turned out to say
The finding that justified the project: a hazard affecting roughly a third of all scheduled trips traces back to just eight platforms. It is invisible to any model that treats accessibility as one flag per station, and it disappears the moment you resolve to the platform instead.
The honest limitation: the live FastAPI service still needs a public host and a scheduled feed rebuild before this can support real travel decisions. The Pages app therefore uses a small, dated demonstration dataset and says so before the form.