After getting feedback, advice, and counsel about my recent public transport mapping updates in OpenStreetMaps, I started adding the missing "40" bus route to the Baltimore Metro Area. Part of the planning was whether to include bus stops and stopping locations, part was identifying errors and omissions in the OSM base, and part was being able to visualize current routes with OSM data.
Then a fragment:
I can see the 40 on existing bus stop signs:
This stop includes the OR (Orange), the 40, 59, 62, and 160 routes. When I looked, the OSM data for stop 7597 showed '59;62;160;OR' while the MTA showed '59;62;160;OR', as if the bus went to the Essex Park + Ride on the other side of the street instead of this stop.
So, I am unsure what the right data should be, until I ride the bus, or observe at the stop.
The
guidance for adding a new route ("Step 1 - Make sure that each bus stop in the route has been added to OpenStreetMap") said check all the nodes. Given the above discrepancy, I hope to add at least one-way next, as small fixes could happen after, like when the routes/schedules change.
The last overview map update page from MTA [
mta.maryland.gov/transit-maps ] is from 2022, and the 40 route is not on the "BaltimoreLink Interactive System Map". That's a map from Google rather than hosted by the government. However, extrapolating shows this page is available in 2025:
Methodology
For myself, and others who may wish to get some GIS mojo working, general steps I've taken to gather data, analyze it, and visualize in various ways to find improvements to OSM content. The earlier post [
https://jspath55.blogspot.com/2025/06/which-bus-such-bus.html ] covers some of this; apologies for repeating myself.
MTA Data
The Maryland Department of Transportation includes the Mass Transit Administration. In addition to the schedule site listed above, there is a GIS portal with bus stops and routes, though I've not gotten much out of the latter.
Home > services > Transportation > MD_Transit (FeatureServer)
Then:
And end up here:
 |
GIS ARC |
Part 2 (right side)
The way to pull the data is via:
https://geodata.md.gov/imap/rest/services/Transportation/MD_Transit/FeatureServer/9/query
https://geodata.md.gov/imap/rest/services/Transportation/MD_Transit/FeatureServer/9/query?where=stop_id+is+NOT+NULL ...
"All" query:
stop_id is NOT NULLOutput fields: stop_id, stop_name, Routes_Served, shelter
Results in a linear format:
stop_name: Cylburn Ave & Greenspring Ave
Routes_Served: 31, 91, 94
Shelter: Yes
stop_id: 1
Point:
X: -8533795.9128
Y: 4772066.8330999985
stop_name: Lanier Ave & Sinai Hospital
Routes_Served: 31, 91, 94
Shelter: No
stop_id: 3
Point:
X: -8534126.0864
Y: 4772153.208300002
[...]
I parsed this into a CSV format to load into QGIS. The wrinkle was finding how to deal with the X Y coordinates returned rather than the expected latitude/longitude pairs. QGIS was happy when I set the coordinate reference system (CRS) to "ESRI:102100 - WGS_1984_Web_Mercator_Auxiliary_Sphere."
OSM data
I used OverPass-Turbo.eu to get extracts of bus stopping locations and bus stops, then separate them.
[out:csv("ref", "public_transport", "name", "route_ref", "highway", "network", "operator", ::lat, ::lon, ::id)];
(
{{geocodeArea:"Maryland"}};
)->.metroArea;
node(area)[bus=yes];
out;
I used "Maryland" rather than "Baltimore" to pick up Anne Arundel County and other nearby locales, though this expanded the return to include eastern and western counties as well as DC Metro. Better too much data than not enough here.
The first tries did not include position or OSM node ID; eventually I found the special names needed to get theses critical columns. After splitting into 2 files to allow QGIS layers for each, over 4,000 bus stops found though under 1,000 stopping locations.
$ wc -l OSM_platforms_20250623.csv OSM_stops_20250623.csv
4240 OSM_platforms_20250623.csv
928 OSM_stops_20250623.csv
5168 total
Sample export:
Add these 2 layers, plus the MTA extract, and start the reconciliation.
Things found:
- MTA and OSM sequence the routes differently, and the MTA records appear in yet another order compared to bus stop signs. I wrote a Perl script to convert the MTA data into the order OSM expects to make it easier to update wrong data.
- The bus stop names differ in upper case/lower case, and various abbreviations ("nb" versus "northbound", for example.
- Typos in both OSM and MTA data conglomerating 2 or more routes to one number ("78150" instead of "78, 150" or "78;150".
- Null data nodes: apparent OSM bus stops without details (e.g. openstreetmap.org/node/2743861086 ). These are presumably orphans, though they may be incomplete nodes that should remain. If no MTA stops nearby, fix me candidates.
- Typos of other sorts, missing stops, incomplete or duplicate descriptions.
Things fixed:
I alternated trying to simplify the updates to be made by only looking at platforms, but decided that version 2 with a stopping location paired with the platform is the preferred way. I created a spreadsheet to have a local index of the 40 bus routes in either direction. The columns included nodes for each type, stop numbers and names, and an alternate name per the official signage ("Eastpoint" as the area for Eastern & 54th, east or west bound).
Only a handful of bus stop areas include a stopping location, so that is my current task. After that, on to steps 2 and 3:
Step 2 - Create the new bus relations
Step 3 - Create a route master relation
The QGIS map view, overlaying MTA, OSM stops/platforms:
By migrating these layers into a PostGIS/PostgreSQL database (using a CRS under 10,000), I can now write SQL queries with outer joins and other comparison techniques.
Stops in MTA but not OSM:
select
ref,
substr(trim(route_ref),1,8) as routes,
trim(name) as name
from
"OSM_platforms_20250623"
where
cast(ref as text) NOT in (
select
field_4
from
"bus-stops-20250611"
)
;
[...]
14166 | 30;34 | Northern Parkway & Park Heights Avenue Westbound Far-side
14215 | | Tradepoint Atlantic Floor & Decor
14216 | | Tradepoint Atlantic Royal Farms
190036 | | Baltimore Greyhound Terminal
3002008 | | Good Luck Rd at Oakland Ave
3003202 | | Good Luck Rd at Parkdale High School
3003205 | | Good Luck Rd at Oakland Ave
(133 rows)
Stops in MTA but not OSM:
select
upper(substr(mta.field_1,1,40)) as nm,
substr(mta.field_2,1,32) as f2,
mta.field_4,
osm.ref
from
"bus-stops-20250611" mta
left outer join
"osm_platforms_20250623_md" osm on mta.field_4 = cast(osm.ref as text)
where
osm.ref is NULL
or
cast(osm.ref as float) <= 0
order by
cast(mta.field_4 as float)
;
This one included DC Metro areas, so needs more tuning to be useful.
Show mismatched route lists per stop:
select
upper(substr(mta.name,1,40)) as nm,
substr(mta.route_osm_style,1,32) as mta_route_osm,
substr(osm.route_ref,1,32) as osm_route,
mta.stop,
osm.ref
from
"bus-stops-20250611+0626" mta
left outer join
"osm_platforms_20250623_md" osm on cast(mta.stop as text) = osm.ref
where
(osm.ref is NULL or cast(osm.ref as text) >= '0')
and
mta.route_osm_style != osm.route_ref
order by
mta.stop
;
The later version of the MTA list includes the reordered route list, as comparing the strings in SQL was not an obvious approach. Example results:
[...]
upper(substr(mta.name,1,40)) as nm,
substr(mta.route_osm_style,1,32) as mta_route_osm,
substr(osm.route_ref,1,32) as osm_route,
mta.stop
[...]
LOCH RAVEN BLVD & STATE HWY DR SB 53 53;103;104 422
LOCH RAVEN BLVD & SAYWARD AVE SB 53;GR 53;103;104;GR 435
SAINT PAUL ST & 31ST ST SB 51;95;SV 95;SV 482
SAINT PAUL ST & 29TH ST SB 51;95;SV 95;SV 483
CHARLES ST & REDWOOD ST NB 51;65;67;95;103;410;411;420;GR;S 51;65;67;95;103;164;410;411;420; 506
SAINT PAUL ST & FAYETTE ST FS SB 67;76;95;103;120;210;215;310;410 67;76;95;103;120;164;210;215;310 516
CHARLES ST & HYATT NB 51;67;94;95;103;SV 51;67;94;95;103;103;164;SV 518
CHARLES ST & FAYETTE ST NB 51;95;103;410;411;420;GR;SV 51;95;103;410;411;420;425;GR;SV 519
CHARLES ST & READ ST NB 51;95;103;GR;SV 51;95;103;410;GR;SV 525
I photographed the bus stop sign at Charles and Read Streets (northbound) only yesterday, confirming the list MTA had. And updated the node:
openstreetmap.org/node/6254842377