← Back to the map LATLONG.SHOP

About the Bike Map of Madison

An interactive map of cycling infrastructure in Dane County, Wisconsin, derived from OpenStreetMap and refreshed monthly. This page documents how the network is extracted and classified, for people who already know the OSM tagging schema.

Overview

The page is a static site (Mapbox GL JS, hosted on GitHub Pages). The bike network is a Mapbox vector tileset built from an OSM extract; a scheduled GitHub Action re-pulls the data, re-classifies it, and re-publishes the tileset. Nothing is hand-drawn: if a segment looks wrong, the fix belongs in OSM. The pinned segment card has "View on OSM" and "Edit" links for the underlying way, and corrections show up after the next refresh (plus tile cache expiry).

Extraction query

Overpass QL, run against the area derived from the county relation (3600000000 + 1799444). It selects candidate ways with any tag that could indicate bicycle infrastructure, and the classifier then decides what each one is.

[out:json][timeout:300];
area(3601799444)->.a;
(
  way["highway"="cycleway"](area.a);
  way["highway"~"^(path|footway|pedestrian|bridleway|track)$"]["bicycle"="designated"](area.a);
  way["highway"]["cycleway"~"^(lane|track|shared_lane|share_busway|shoulder|opposite|opposite_lane|opposite_track|opposite_share_busway)$"](area.a);
  way["highway"]["cycleway:both"~"…same regex…"](area.a);
  way["highway"]["cycleway:left"~"…same regex…"](area.a);
  way["highway"]["cycleway:right"~"…same regex…"](area.a);
  way["highway"]["bicycle_road"="yes"](area.a);
  way["highway"]["cyclestreet"="yes"](area.a);
);
out tags geom;

/* curated route relations, from pipeline/signed_routes.txt */
rel(id:3896886,11242307,…)->.sr;
.sr out body;
way(r.sr)(area.a);
out tags geom;

The first block selects ways by tag. The second adds the member ways of a short, hand-picked list of route=bicycle relations (see Signed routes). Nodes and all other relations are not used. Results come from the public Overpass API with fallback endpoints; the job retries and fails rather than publishing a partial pull (see Refresh pipeline).

Classification

Each candidate way receives at most one class. A way never appears twice, even when cycleway:left and cycleway:right differ: the best-protected value wins and the raw per-side tags are kept as properties.

Exclusions

Dropped before classification, regardless of other tags:

ConditionReason
highway ∈ proposed, construction, abandoned, razed, planned, disusedNot usable infrastructure
bicycle ∈ no, dismountBikes not permitted or must dismount
access ∈ private, noNo public access

Not excluded: access=customers/permissive/destination and similar, bicycle=permissive, and surface type. Unpaved paths are included; surface is shown on the card.

Decision procedure

First match wins. Steps are evaluated in this order:

  1. highway=cycleway → permit when fee=yes or access=permit; otherwise path if the way's name is on the curated trail list; otherwise cycleway.
    facility (display label): foot ∈ {yes, designated, permissive} and segregated=yes → "Segregated shared-use path"; foot in that set otherwise → "Shared-use path"; else "Bike path". Any cycleway* tags on a cycleway are ignored.
  2. highway ∈ {path, footway, pedestrian, bridleway, track} with bicycle=designated → path if the name is on the curated list, otherwise pathway (facility "Designated path"). No fee/permit distinction is made here.
  3. Road-side facilities. Read cycleway, cycleway:both, cycleway:left, cycleway:right, map each value through the table below, and take the lowest-rank (best protected) class found.
  4. Signed ways with no other facility. If the way is signed (see below) and steps 1–3 found nothing, it becomes route (facility "Signed route"). A signed way that does have a facility keeps that class: a cyclestreet with cycleway=lane is a lane that is also signed.
  5. Otherwise the way is not part of the network.

Signed routes

"Signed or Well-Known Route" is an overlay on top of the classes, not a class you have to be limited to. A way is signed when either of these is true:

Signed ways get signed=1 and, for relation members, routes with the relation names. A signed way keeps its normal class (shared lane, trail, and so on) and the map adds a glow behind it. Only a signed way with no other bike facility gets class route, drawn as a dotted line. Local trail and greenway relations (Fitchburg, Middleton, the state trails) are intentionally not in the list: those paths are already Bike Trail or Off-Street Bike Lane.

Curated trail list

"Bike Trail or Path" is deliberately narrow: it is for well-known, named trails (the Southwest Path, Capital City State Trail, Cannonball Path, and so on), not for every highway=cycleway. Membership is decided by a hand-maintained allowlist, pipeline/trail_names.txt.

cycleway=* value mapping

Applies to step 3, for each of the four keys above.

Tag valueClassNotes
track, opposite_tracktrackPhysically separated track, tagged on the road way
lane, opposite_lane, oppositelaneopposite is treated as an opposite-direction lane
shouldershoulderPaved shoulder usable by bikes
shared_lane, share_busway, opposite_share_buswaysharedSharrows and shared bus lanes
separateignoredThe path is its own way; it is captured through step 1 or 2
no, none, anything elseignoredIncluding legacy shared, sidewalk, crossing

Only the main cycleway* values are read. Subtags such as cycleway:left:lane=exclusive, :separation, :buffer, :segregated, and :width are not used for classification.

Classes and rank

rank orders classes by protection (lower is better). It decides which feature the hover card shows when lines overlap, and the label priority for route names.

classrankSource tags (summary)
path1Off-street way (step 1 or 2) whose name is on the curated trail list
permit1highway=cycleway + (fee=yes or access=permit)
cycleway2Any other highway=cycleway
track2cycleway[:side]=track | opposite_track
pathway2highway=path|footway|pedestrian|bridleway|track + bicycle=designated, not on the curated list
route7Signed way with none of the facilities above
lane4cycleway[:side]=lane | opposite_lane | opposite
shoulder5cycleway[:side]=shoulder
shared6cycleway[:side]=shared_lane | share_busway | opposite_share_busway

Output data model

The tileset has one source layer, bike (LineString features, zoom 8–14), with one feature per OSM way. Route-name labels come from a separate file, described below. Feature properties on bike are the derived fields plus a pass-through of raw OSM tag values. Keys containing a colon are written with an underscore (cycleway:left → cycleway_left). Tag values are not normalised.

PropertyTypeDescription
osm_idintegerOSM way id. Used as the feature id (promoteId) for hover state.
classstringOne of the nine classes above.
rankintegerProtection rank of class.
facilitystring, optionalHuman label for off-street ways (steps 1–2 only).
Pass-through OSM tags, present only when the way has them
name, ref, operatorstringUsed for card titles and route-name labels.
highwaystringThe way's own highway value.
cycleway, cycleway_both, cycleway_left, cycleway_rightstringRaw side tags, kept so asymmetric streets can be read in full.
signedinteger, optional1 when the way is signed (see above).
routesstring, optionalNames of the listed route relations the way belongs to, separated by "; ".
bicycle_road, cyclestreetstringRaw values.
bicycle, foot, segregated, access, feestringAccess and use tags.
surface, smoothness, width, inclinestringPhysical attributes.
lit, oneway, maxspeed, bridge, tunnelstringRaw values (for example maxspeed is whatever the mapper entered).

Label lines: data/bike-names.geojson

Used only for the route-name text along the lines. It is a small GeoJSON file the page loads directly, not part of the tileset (line labels placed unreliably on the vector tiles at z12–13, and the GeoJSON path doesn't have that problem). OSM often splits one trail into dozens of ways, and a line label needs a long enough continuous line, so the pipeline builds a second set of features:

A data/manifest.json file is committed on each run with the OSM data timestamp (osm3s.timestamp_osm_base), per-class way counts and lengths, and a content hash.

Map categories and display rules

The map legend follows a print map's symbology, which groups the classes above:

Legend categoryClassesSymbolDrawn fromRoute names
Bike Trail or Pathpath (curated names)Blue edges, light fillz9Yes
Bike Trail (permit required)permitBlue edges, gold fillz9Yes
Off-Street Bike Lanecycleway, pathway, trackSolid bluez9Yes
Dedicated Bike LanelaneDashed bluez11No
Shared Bike Laneshared, shoulderDashed light bluez11No
Signed or Well-Known Routeroute, plus the glow on any signed wayPale-blue glow and white line under any signed segment; dots on top when there is no other facilityz11No

Refresh pipeline

  1. A GitHub Action runs monthly (the 1st, 09:17 UTC) and can be started manually.
  2. fetch_osm.py runs the query above, trying up to three Overpass endpoints with retries, then classifies every way. If fewer than 500 features come out, the run aborts rather than publishing a suspect pull.
  3. Features are written as line-delimited GeoJSON, sorted by osm_id, with coordinates rounded to 7 decimals. A SHA-256 of that file is compared with the previous run. The merged label lines are written separately.
  4. If the content changed (or the run is forced), the file replaces the tileset source and the Mapbox Tiling Service recipe is updated and published (source layer bike, zoom 8–14). Unchanged data is not re-published.
  5. The manifest is committed back to the repository every run.

Known limitations

Attribution

Bike network data © OpenStreetMap contributors, available under the Open Database License. Basemap © Mapbox.

Map and data pipeline by Stephen Kennedy of LATLONG.SHOP.