Modeling Turn Restrictions and One-Way Streets in Catchments
This page solves one exact task: making sure the routing graph honours one-way tags, turn restrictions and access rules, and quantifying how much a catchment changes when it does not.
These are the constraints that separate a network a car can drive from a network a car can see. They cost import time and add complexity, and every routing engine will happily build a graph without them — producing catchments that are systematically too generous in exactly the dense urban markets where a retail decision is most expensive.
Prerequisites
- A built graph with a profile you control, per preparing OSM network extracts for routing.
- A source extract that retains relations, since turn restrictions are relations rather than tags on ways. An extract filtered down to ways alone silently discards every one of them.
- Python packages:
httpxfor probing the engine andgeopandasfor comparing contour geometry. Install withpip install httpx geopandas. - A dense urban test market, because that is where the effect is measurable; a suburban market will show almost nothing and prove almost nothing.
Configuration and execution parameters
| Parameter | Value for this task | Notes |
|---|---|---|
oneway_tags |
honoured | Includes reversed and conditional forms |
turn_restrictions |
imported | Requires relations in the extract |
access_tags |
honoured | Private, delivery-only, permit-required |
barriers |
honoured | Gates, bollards, height and weight limits |
u_turn_penalty_s |
20 | Legal but slow; a zero penalty invents shortcuts |
restriction_scope |
car profile | Walking and cycling ignore most of them |
conditional_handling |
worst case or time-aware | State which; do not leave it implicit |
u_turn_penalty_s is the setting nobody sets and everybody should. With no penalty, the engine treats a U-turn as free, so a divided highway becomes trivially crossable and a catchment reaches across it as if the central reservation were not there. Twenty seconds is enough to make the engine prefer a legitimate junction where one exists nearby.
Annotated implementation
Restrictions are imported by the engine rather than applied by your code, so the work is in the profile, the extract and the verification. The probe below measures the difference between two builds that differ only in restriction handling.
from __future__ import annotations
import geopandas as gpd
import httpx
from pyproj import CRS
METRIC_CRS = CRS.from_epsg(5070)
def contour_area_km2(engine: str, lon: float, lat: float,
seconds: int = 900) -> float:
"""Area of a single contour, measured in an equal-area CRS."""
with httpx.Client(base_url=engine, timeout=60.0) as client:
r = client.get(f"/isochrone/v1/driving/{lon},{lat}",
params={"contours_seconds": seconds, "polygons": "true"})
r.raise_for_status()
gdf = gpd.GeoDataFrame.from_features(r.json()["features"], crs="EPSG:4326")
return float(gdf.to_crs(METRIC_CRS).area.sum() / 1e6)
def compare_builds(stores: gpd.GeoDataFrame, engine_with: str,
engine_without: str) -> gpd.GeoDataFrame:
"""How much larger is the catchment when restrictions are ignored?"""
rows = []
for row in stores.itertuples():
lon, lat = row.geometry.x, row.geometry.y
with_r = contour_area_km2(engine_with, lon, lat)
without_r = contour_area_km2(engine_without, lon, lat)
rows.append({
"store_id": row.store_id,
"density_band": row.density_band,
"area_with_km2": round(with_r, 2),
"area_without_km2": round(without_r, 2),
# Positive means the unrestricted graph claims more reach.
"overstatement_pct": round(100 * (without_r - with_r) / with_r, 1),
})
return gpd.GeoDataFrame(rows)
Running this once per market is enough to settle the argument about whether restrictions are worth importing, and it produces a number that can be quoted rather than an assertion that has to be believed.
Failure modes and debugging
Relations stripped during extraction. The commonest reason a graph has no turn restrictions is that the extract never contained them. Filters that keep only highway ways discard the relations that encode restrictions, and the import proceeds with no warning because an extract with zero restrictions is perfectly valid. Checking the relation count in the extract before import catches it in one line.
Conditional restrictions treated as absolute. Many restrictions apply only at certain times — no left turn between seven and nine in the morning, for example. Importing them as permanent makes the graph pessimistic for most of the day; ignoring them makes it optimistic at peak. Choose one and record the choice; for a retail catchment measured at trading hours, the trading-hour interpretation is usually the right one and is rarely the default.
Access tags applied inconsistently. A service road tagged for delivery access only is legitimately usable by a delivery vehicle and not by a shopper, so the same physical road belongs in one profile and not another. Applying a single access policy across profiles produces either shoppers routing through service yards or delivery planning that cannot reach loading docks.
Zero U-turn penalty. Easy to miss and consistently wrong. The symptom is a contour that crosses a divided highway at arbitrary points rather than at junctions, which looks plausible on a small map and is materially wrong about which side of the road a customer lives on.
Verification
- Count relations in the extract before import and after clipping. A clip that dropped relations is a two-minute fix and an otherwise invisible defect.
- Probe a known banned turn. Request a route across a restriction you have verified on the ground or in the map, and confirm the engine takes the detour. One route is enough to prove the restrictions loaded.
- Compare contour areas between restricted and unrestricted builds on a sample per density band, as above. This is the number that justifies the import cost.
- Check the U-turn behaviour explicitly by routing between two points on opposite sides of a divided highway with no nearby junction; the duration should reflect the detour.
Frequently Asked Questions
Do restrictions matter for a fifteen-minute catchment, or only for turn-by-turn navigation?
They matter more for the catchment, because the error compounds across every route rather than affecting one journey. A navigation instruction that ignores a banned turn is wrong once and obviously; a catchment computed on a graph without restrictions is wrong everywhere, by a few per cent, in a direction that always favours the same kind of site. The second failure is far harder to notice and considerably more expensive.
Should the walking profile honour one-way tags?
Almost never for pedestrians, and yes for cycling in most jurisdictions. Applying vehicle one-way rules to a walking graph makes pedestrian catchments absurdly detoured in exactly the dense centres where walking matters. This is a good example of why one graph per profile beats one graph with several costings applied at query time — the structural rules differ, not just the speeds.
What about restrictions the map does not record?
They are the residual error, and the honest response is to measure it rather than assume it away. Comparing modelled durations against a sample of observed trips — from delivery telematics or loyalty data with timestamps — gives a market-level correction factor and, more usefully, identifies the junctions where the model and reality disagree most. Those are usually a handful per market and often a data-quality contribution worth making back to the map.
Is there a cheap approximation if restrictions cannot be imported?
A blunt one: apply a market-level penalty to durations scaled by restriction density, calibrated from the comparison in this page. It is better than nothing and much worse than importing the restrictions, because it corrects the average while leaving every individual catchment the wrong shape. Treat it as a stopgap with a date attached rather than as a design.
How do restrictions interact with the contour’s shape rather than its size?
They make it lumpier and more directional, which is the part a map reader notices first. An unrestricted contour in a grid tends toward a smooth diamond; a restricted one grows further along the streets whose turns are permitted and stalls where a one-way pair blocks the natural path. That shape carries information — it says which approaches a store is genuinely convenient from — and it is the reason a catchment drawn on a restricted graph is worth showing to a real-estate team rather than only feeding to a model.
Should restriction handling differ between screening and diligence?
It can, and the honest version states it. Screening thousands of candidates on a graph with restrictions costs build time and query time; running the screen without them and the shortlist with them is a defensible compromise so long as the difference is measured, since the screen then carries a known, systematic bias toward dense-market sites. What is not defensible is doing this by accident, discovering the discrepancy when a shortlisted site’s catchment shrinks, and having no record of which stage produced which number.
Do restrictions change how a contour should be cached?
Only through the build version, which already covers it. Restriction handling is part of the profile and the profile is part of the build identifier, so a change to it invalidates the cached contours exactly as a network change would. What is worth avoiding is treating restriction handling as a request-time option, which would make two contours with the same cache key describe different worlds.
Related
- Preparing OSM Network Extracts for Routing — where restrictions enter the build.
- Tuning Routing Profiles for Retail Vehicle Types — the profile that decides which restrictions apply.
- Configuring OpenRouteService for Drive-Time Maps — the request-side parameters that interact with these rules.