A look at OpenStreetMap data in Madison and Milwaukee
This is a very niche post, but maybe it’s of interest to some niche people and so I may as well put it on my blog. Last week I learned about a change in how People for Bikes calculate “Bicycle Level of Traffic Stress” for a specific type of street: Highways classified in OpenStreetMap as tertiary used to be classified low-stress as long as they had painted bike lanes; now they are classified as high-stress. Makes sense: Tertiary highways are probably busier, with higher speeds, and so a painted bike lane is not enough to make them feel safe for people of all ages and all abilities. The People for Bikes analysis assumes a default speed of 30 mph on these streets if no explicit speed limit is tagged in OpenStreetMap. As I looked at the map for Madison to see which streets are classified as highway=tertiary I noticed that a lot of them actually have speed limits as low as 20 mph, but those speed limits often weren’t in OpenStreetMap. This warranted more systematic investigation:
maxspeed=* tags set?We obtain all tertiary roads in the greater Madison area, as well as the city boundaries for Madison:
madison_city_limits <- places(state = "WI") |> filter(NAME == "Madison") |> st_transform(crs = "EPSG:4326")
bb <- c(-89.594193, 42.997023, -89.202118, 43.166131)
x <- opq (bbox = bb) |>
add_osm_feature (key = "highway", value = "tertiary") |>
osmdata_sf ()
tertiaries <- x$osm_lines
Now we filter to only the highways that are within Madison or have at least a portion within Madison.
msn_tertiary <- tertiaries[madison_city_limits, , op = st_intersects]
msn_tertiary |>
mutate(maxspeed_numeric = as.numeric(str_remove(maxspeed, " mph")),
maxspeed_factor = as_factor(maxspeed)) |>
st_drop_geometry() |>
count(maxspeed_numeric) |>
gt::gt()
| maxspeed_numeric | n |
|---|---|
| 15 | 3 |
| 20 | 14 |
| 25 | 344 |
| 30 | 306 |
| 35 | 153 |
| 40 | 23 |
| 45 | 5 |
| 50 | 1 |
| NA | 1033 |
We see that roughly half of tertiary highways have missing speed limit tags. But of the ones that do have it, many of them have limits lower than the 30 mph that the Brokenspoke analysis assumes. Note that this is based on OSM segments without taking their length into account.
We can also map this data to get a better sense of the spatial distribution. One may hypothesize that the lower speed limit roads are closer to the center of Madison.
library(tmap)
tmap_mode("plot")
msn_tertiary |>
mutate(maxspeed_numeric = as.numeric(str_remove(maxspeed, " mph")),
thirty_mph = case_when(maxspeed_numeric < 30 ~ "below 30 mph",
maxspeed_numeric == 30 ~ "30 mph",
maxspeed_numeric > 30 ~ "over 30 mph"
)) |>
tm_shape() +
tm_lines(col = "thirty_mph")

This seems to confirm that hypothesis: In central areas, a lot of tertiary roads have limits below 30 mph, whereas those with limits above 30 are in the peripheral areas.
Let’s repeat the analysis for Milwaukee.
milwaukee_city_limits <- places(state = "WI") |> filter(NAME == "Milwaukee") |> st_transform(crs = "EPSG:4326")
bb <- c(-88.205109, 42.845378, -87.782135, 43.219020)
x <- opq (bbox = bb) |>
add_osm_feature (key = "highway", value = "tertiary") |>
osmdata_sf ()
tertiaries <- x$osm_lines
mke_tertiary <- tertiaries[milwaukee_city_limits, , op = st_intersects]
mke_tertiary |>
mutate(maxspeed_numeric = as.numeric(str_remove(maxspeed, " mph")),
maxspeed_factor = as_factor(maxspeed)) |>
st_drop_geometry() |>
count(maxspeed_numeric) |>
gt::gt()
| maxspeed_numeric | n |
|---|---|
| 20 | 4 |
| 25 | 360 |
| 30 | 2015 |
| 35 | 175 |
| 40 | 22 |
| NA | 211 |
Speed limit data on Milwaukee streets is more completed, and a large majority of them indeed have a speed limit of 30 mph.
mke_tertiary |>
mutate(maxspeed_numeric = as.numeric(str_remove(maxspeed, " mph")),
thirty_mph = case_when(maxspeed_numeric < 30 ~ "below 30 mph",
maxspeed_numeric == 30 ~ "30 mph",
maxspeed_numeric > 30 ~ "over 30 mph"
)) |>
tm_shape() +
tm_lines(col = "thirty_mph")

The spatial distribution of 30 vs <30 mph streets is more even than in Madison. But streets with a speed limit above 30 mph are again at the edges of the city.
The takeaway? Assuming a speed limit of 30 mph for tertiary roads overall is probably reasonable. But in a city like Madison it does lead to misclassification of streets as high stress. I am working on adding explicit maxspeed=* data to as many tertiary highways I can. Good Mapillary coverage helps!
If you want to review the data for your city, you can use this Overpass query:
[out:json][timeout:25];
(
way["highway"="tertiary"][!"maxspeed"]["cycleway"]({{bbox}});
way["highway"="tertiary"][!"maxspeed"]["cycleway:left"]({{bbox}});
way["highway"="tertiary"][!"maxspeed"]["cycleway:right"]({{bbox}});
way["highway"="tertiary"][!"maxspeed"]["cycleway:both"]({{bbox}});
);
out body;
>;
out skel qt;
For attribution, please cite this work as
Kliems (2026, Feb. 3). Harald Kliems: Speed limits on tertiary highways and traffic stress. Retrieved from https://haraldkliems.netlify.app/posts/2026-02-03-speed-limits-tertiary-higways-traffic-stress/
BibTeX citation
@misc{kliems2026speed,
author = {Kliems, Harald},
title = {Harald Kliems: Speed limits on tertiary highways and traffic stress},
url = {https://haraldkliems.netlify.app/posts/2026-02-03-speed-limits-tertiary-higways-traffic-stress/},
year = {2026}
}