Comparing OSRM vs Valhalla for retail catchment analysis
When evaluating site viability for urban retail expansion, location intelligence teams must transition from simple Euclidean buffers to network-constrained catchment polygons that reflect actual pedestrian, transit, and vehicular accessibility. The architectural divergence between OSRM and Valhalla directly impacts the accuracy of these multi-modal drive-time and walk-time polygons, particularly when integrating Isochrone Generation & Network Analysis into automated site selection pipelines. While both engines consume OpenStreetMap data, their routing graph construction, costing parameterization, and error propagation behaviors require distinct configuration strategies.
Graph Architecture & Costing Paradigms
The primary differentiator lies in how each engine handles multi-modal costing. OSRM relies on a monolithic routing profile compiled via Lua, where pedestrian, bicycle, and automotive routing are isolated into separate pre-built graphs. This architecture delivers exceptional query latency via contraction hierarchies but requires explicit profile switching and post-processing to merge overlapping accessibility zones. For retail planners, this means generating separate drive-time and walk-time layers and performing spatial unions downstream.
Valhalla implements a unified graph with dynamic costing_options that allow real-time weighting of transit transfers, walking penalties, and road classifications within a single request. This design enables native computation of hybrid isochrones without external graph stitching. When implementing Implementing Multi-Modal Routing for Urban Retail, the most critical configuration parameters are the costing model and the contours time thresholds.
| Dimension | OSRM | Valhalla |
|---|---|---|
| Graph model | Monolithic, per-profile pre-built graphs | Unified graph with dynamic costing |
| Multi-modal routing | Separate graphs, stitched downstream | Native within a single request |
| Isochrone output | Sampled via /table + interpolation |
Native /isochrone GeoJSON endpoint |
| Costing control | Lua profile, compile-time | Runtime costing_options JSON |
| Query latency | Sub-second, contraction-hierarchy optimized | Higher, but more flexible |
| Best retail fit | High-throughput distance matrices | Multi-modal catchment polygons |
Production-Ready Python Implementation
The following implementation enforces strict coordinate validation, standardizes response parsing for retail site evaluation workflows, and includes retry logic and Shapely-based polygon validation.
import requests
import logging
from typing import Dict, Any, Optional
from shapely.geometry import shape, Polygon, MultiPolygon
from shapely.validation import make_valid
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
class RetailCatchmentRouter:
def __init__(
self,
osrm_base: str,
valhalla_base: str,
timeout: int = 30,
max_retries: int = 3
):
self.osrm_base = osrm_base.rstrip("/")
self.valhalla_base = valhalla_base.rstrip("/")
self.timeout = timeout
self.max_retries = max_retries
self.session = requests.Session()
self.session.headers.update({
"Accept": "application/json",
"Content-Type": "application/json"
})
def _validate_coordinates(self, lat: float, lon: float) -> None:
if not (-90.0 <= lat <= 90.0) or not (-180.0 <= lon <= 180.0):
raise ValueError(f"Invalid WGS84 coordinates: lat={lat}, lon={lon}")
def query_valhalla_isochrone(
self,
lat: float,
lon: float,
minutes: int,
costing: str = "auto"
) -> Optional[Dict[str, Any]]:
"""
Fetch a native Valhalla isochrone with retail-optimized costing parameters.
Valhalla's /isochrone endpoint accepts lat/lon (not lon/lat).
"""
self._validate_coordinates(lat, lon)
url = f"{self.valhalla_base}/isochrone"
payload = {
"locations": [{"lat": lat, "lon": lon}],
"costing": costing,
"contours": [{"time": minutes, "color": "ff0000"}],
"costing_options": {
"auto": {
"use_ferry": 0.5,
"use_toll": 0.0,
"maneuver_penalty": 5.0
},
"pedestrian": {
"walking_speed": 5.0,
"use_ferry": 0.0,
"use_living_streets": 0.8
},
"transit": {
"use_bus": 0.5,
"use_rail": 0.8,
"transfer_penalty": 120
}
},
"polygons": True,
"denoise": 1.0,
"generalize": 0.0
}
for attempt in range(self.max_retries):
try:
response = self.session.post(url, json=payload, timeout=self.timeout)
response.raise_for_status()
data = response.json()
if "features" not in data or not data["features"]:
raise RuntimeError("Valhalla response missing valid isochrone features.")
return data
except requests.exceptions.RequestException as e:
logger.warning("Valhalla attempt %d/%d failed: %s", attempt + 1, self.max_retries, e)
if attempt == self.max_retries - 1:
return None
return None
def parse_to_valid_polygon(self, response: Dict[str, Any]) -> Optional[Polygon]:
"""Extract, validate, and clean the primary catchment polygon."""
if not response or "features" not in response:
return None
try:
feature = response["features"][0]
geom = shape(feature["geometry"])
if not geom.is_valid:
geom = make_valid(geom)
# Return the largest polygon if the result is a MultiPolygon
if isinstance(geom, MultiPolygon):
return max(geom.geoms, key=lambda g: g.area)
return geom if isinstance(geom, Polygon) else None
except (KeyError, IndexError, ValueError) as e:
logger.error("GeoJSON parsing failed: %s", e)
return None
Deployment & Pipeline Optimization
For enterprise retail analytics, OSRM is the better choice for high-throughput origin-destination matrix calculations (fleet routing, delivery zone optimization) where contraction-hierarchy latency is critical. Valhalla is the better choice when you need native multi-modal catchment polygons in a single API call, because OSRM’s /table endpoint only returns cost matrices and requires a separate rasterization-and-contouring step to produce polygons.
When scaling batch isochrone generation, implement exponential backoff and coordinate deduplication to respect rate limits. Retail planners should cache polygon geometries using spatial hashing (e.g., H3 or S2 grids) to avoid redundant network queries for overlapping site evaluations. Polygon validation via shapely.validation.make_valid() is non-negotiable; malformed geometries will break downstream spatial joins with census demographics or footfall datasets.
For authoritative API specifications and parameter tuning, consult the official Valhalla API Documentation and the OSRM HTTP API Reference. Geometry validation and spatial operations should follow the Shapely Documentation standards to ensure topological integrity across GIS environments.
By aligning costing models with retail accessibility requirements and enforcing strict error handling, location intelligence teams can deploy automated catchment analysis pipelines that scale across regional portfolios while maintaining sub-metre spatial accuracy.