The Ohio Turnpike (OTIC): a ticket system between Tolling Points 49 (Swanton) and 211 (Newton Falls), priced entry to exit from the printed Schedule of Tolls (8 classes x E-ZPass, Video E-ZPass, Cash/Credit Card, Unpaid Toll), plus the Westgate barrier (MP 4, both directions) and the Eastgate barrier (MP 239, westbound only). The exits west of 49 and east of 211 are free. The Memorial Bridge (Parkersburg) is in the WV pages.
otic_schedule_page. Put the new PDF in urls.txt as otic_schedule_<year> and the new calculator file /js/rate_calculator_<year>_combined.js, then change the two names in oh_state.py.oh_state.py refuses to build unless every printed cell equals OTIC's calculator data (13,632 trip cells and 96 barrier cells today).bash chain.sh OH (builder, OSM street names, snap, road ties, builder). OSM extract: osm/oh_* (box -84.83,38.40,-80.51,41.98; made by ~/maps/osm/osm10.sh), then python3 tollnode_ways.py OH.
otic_map_points) with the OSM toll nodes nearest each (within 2.5 km). Exits 118, 142 and 145 have no OSM toll node: their points are the outermost tolled ramp / connector ways (OSM toll=yes) where they meet an untolled road (estimated).toll_ways.py treats a trip-priced point without "gantry" in its name and without OSM nodes as a position-only marker.osm/oh_not_toll_nodes.json).Hosts: geo1 (192.168.50.220) holds the sources, builders, drafts, map and API; b100 (192.168.50.14) is the test client. All times are Eastern.
| what | where (geo1 unless said) |
|---|---|
| sources list | /home/geo1/maps/toll/OH/urls.txt (name <tab> URL) |
| archived sources | /home/geo1/maps/toll/OH/sources/<name>/<YYYY-MM-DD>/ (file + meta.json: url, fetched, sha256) |
| builder | /home/geo1/system/handbuilt/oh_state.py; shared code toll_common.py |
| OpenStreetMap inputs | /home/geo1/system/handbuilt/osm/oh_* (cut from /home/geo1/maps/osm/us-latest.osm.pbf) |
| output | draft /srv/toll/draft/OH/ (the builders write here); published /srv/toll/OH/ (copied from the draft by promote.py publish). The map and the API read the published page, or the draft while it is listed for review |
| toll engine | source /home/geo1/maps/web/tolls.js, served copy /srv/maps/tolls.js (the API runs the served copy) |
| API | /home/geo1/maps/api/server.mjs on 127.0.0.1:8090 behind nginx /api/; pid in logs/test.pid |
| regression trips | b100: ~/apitest/site/cases.json, runner ~/apitest/run.py, browser check ~/apitest/browser.mjs |
python3 /home/geo1/system/handbuilt/fetch_list.py OH [name ...] (no names = every line of urls.txt). A new source: add a line to urls.txt first, then fetch it by name.node ~/apitest/fetch_b100.mjs <out> <name>=<url> ..., then rsync -a <out>/ geo1@192.168.50.220:/home/geo1/maps/toll/OH/sources/. Its meta.json records that it came from b100.diff, pdftotext -layout for PDFs). Same content: nothing to do. Changed amounts or a new effective date: rebuild (step 2) and update the expected amounts of the affected regression trips from the operator's own figures.python3 /home/geo1/maps/toll/bin/tollup.py OH update fetches the adapter's sources (bin/adapters/oh.py), stores a document only when its content changes, writes one rate file per version under /home/geo1/maps/toll/OH/rates/ and appends every price change to history.csv (every run to runs.log). TOLL_WEB=/home/geo1/maps/toll/corpus python3 /home/geo1/maps/toll/bin/publish.py OH writes the corpus /home/geo1/maps/toll/corpus/OH from it (DE: plain publish.py DE writes /srv/toll/DE). Never hand-edit either. Then rebuild the draft (step 2) so it picks up the new rates.
cd /home/geo1/system/handbuilt && python3 oh_state.pybash /home/geo1/system/handbuilt/chain.sh OH (logs in /tmp/oh1.log ... oh6.log; it prints the points, any point on no road and the untied count). The builders written since October 2026 share state_kit.py (sources, booths, terms, rows, write) and own_state.py (keeps a state's untied list to toll nodes inside the state, by the local geocoder; cache osm/node_state.json).cd /home/geo1/system/handbuilt; H=$(ls osm/oh_highways_*.geojsonseq); J=$(ls osm/oh_junctions_*.geojsonseq)
python3 oh_state.py && python3 osm_streets.py OH $H $J && python3 oh_state.py && python3 osm_snap.py OH && python3 toll_ways.py OH && python3 oh_state.pytoll_ways.py's output: nodes claimed by two points: {} and points on no road: [] must both be empty.OSM way <id>) needs that way in osm/oh_lane_ways.json (the builder writes it). A point on a road priced entry to exit needs gantry in its location name, or toll_ways.py treats it as a position-only exit marker.osm/ny_not_toll_nodes.json (congestion-zone gantries that are not tolls); every state's untied list leaves those out, so rebuild NJ and PA after NY.tolls.js when it starts. Restart it: cd /home/geo1/maps/api && kill $(cat logs/test.pid); (nohup /home/geo1/maps/node/bin/node server.mjs > logs/server.log 2>&1 & echo $! > logs/test.pid) (it is started by hand, not a service).python3 ~/apitest/run.py ~/apitest/site/cases.json. Every trip must be OK. Add a trip for every rate or rule you change, with the amount taken from the operator's own source.tolls.js): edit /home/geo1/maps/web/tolls.js, check with /home/geo1/maps/node/bin/node --check, copy to /srv/maps/tolls.js with sudo install -o www-data -g www-data -m 644, restart the API, run the trips.The builders match toll points to the OSM way ids the router drives, so the extracts here must come from the same us-latest.osm.pbf the router (Valhalla, /home/geo1/maps/valhalla_data) was built from (now 2026-09-27). New OSM data means: rebuild the router tiles, then for each state cut the box, rebuild its OSM inputs and run the full chain, then compare untied_toll_nodes.csv and the estimated positions below. The box for OH and the cutting commands are in the state's procedure above; the toll-node and way index comes from python3 tollnode_ways.py OH.
A rebuild writes the draft (/srv/toll/draft/OH); the published page (/srv/toll/OH) does not change. python3 /home/geo1/maps/toll/bin/promote.py review OH lists the draft for review: the map and the API then read it instead of the published page. Run the regression trips from b100. After approval, python3 /home/geo1/maps/toll/bin/promote.py publish OH copies the draft to /srv/toll/OH (the previous page is kept under /home/geo1/maps/toll/OH/published/<time>/) and takes it off the review list. After a change, update the coverage line for OH in the API page (/api.html, generator /home/geo1/maps/api/doc/api_page.py) and rebuild this page: python3 /home/geo1/system/handbuilt/maintain_pages.py OH.
Each fetch is kept in its own dated folder under /home/geo1/maps/toll/OH/sources/<name>/; the builder always reads the newest one, so a new fetch takes effect at the next rebuild and the old one stays for comparison.
| source | fetched (newest) | fetches kept | how | listed in urls.txt | rows using it |
|---|---|---|---|---|---|
otic_map_pointshttps://www.ohioturnpike.org/js/maps_modernization.js | 2026-10-09 | 1 | fetch_list.py | yes | |
otic_rate_calculator_2026https://www.ohioturnpike.org/js/rate_calculator_2026_combined.js | 2026-10-09 | 1 | fetch_list.py | yes | |
otic_schedule_2026https://www.ohioturnpike.org/docs/default-source/schedule-of-tolls/new-toll-system/2026-sc | 2026-10-09 | 1 | fetch_list.py | yes | 14125 |
otic_schedule_pagehttps://www.ohioturnpike.org/schedule-of-tolls-(2024-2028) | 2026-10-09 | 1 | fetch_list.py | yes | |
otic_toll_rates_pagehttps://www.ohioturnpike.org/e-zpass/toll-rates | 2026-10-09 | 1 | fetch_list.py | yes |
| id | facility | operator | points | rate / trip rows | effective dates in the rows | source |
|---|---|---|---|---|---|---|
OH-TPK | Ohio Turnpike | Ohio Turnpike and Infrastructure Commission (OTIC) | 24 | 14124 | 2026-01-01 | otic_schedule_2026 |
| point | facility | location | placed on | why |
|---|---|---|---|---|
OH-TPK-11 | Ohio Turnpike | 118 Sandusky-Norwalk ramp plaza (gantry) | the outermost tolled ramp ways (OSM toll=yes) where they meet the local road; OSM maps no toll node | R3 estimate: no OSM toll node at Interchange 118; a booth on each tolled ramp way that meets an untolled road nearest OTIC's point |
OH-TPK-14 | Ohio Turnpike | 142 Lorain Co. West ramp plaza (gantry) | the outermost tolled ramp ways (OSM toll=yes) where they meet the local road; OSM maps no toll node | R3 estimate: no OSM toll node at Interchange 142; a booth on each tolled ramp way that meets an untolled road nearest OTIC's point |
OH-TPK-15 | Ohio Turnpike | 145 Lorain-Elyria ramp plaza (gantry) | the outermost tolled ramp ways (OSM toll=yes) where they meet the local road; OSM maps no toll node | R3 estimate: no OSM toll node at Interchange 145; a booth on each tolled ramp way that meets an untolled road nearest OTIC's point |
R2a: 21, R3: 3, built: 1, direction: 2, not: 1, recorded: 1 (all in decisions.csv).
None.
Toll booths / gantries mapped in OpenStreetMap on or near this state's roads that no toll point claims; a route through one lists it under unpriced. After an OSM refresh, look at new ones here (untied_toll_nodes.csv): a new gantry on a priced road means a point is missing.
Run from b100 (never on geo1): python3 ~/apitest/run.py ~/apitest/site/cases.json. Every trip must come back OK; a changed amount is either a rate change (update the expected amount from the operator's own source) or a fault.
| trip | expected |
|---|---|
| OH Turnpike Indiana line -> Pennsylvania line, E-ZPass car (Westgate $2.25 + 49->211 $13.75) | $16.00 |
| OH Turnpike Pennsylvania -> Indiana line, E-ZPass car (Eastgate $3.00 + $13.75 + $2.25) | $19.00 |
| OH Turnpike Pennsylvania -> Indiana line, TxTag (account_tx, not E-ZPass): cash rates | $23.50 |
| OH Turnpike Sandusky-Norwalk (118) -> Lorain-Elyria (145), cash car ($2.75) | $2.75 |