Setting Turn Restrictions in GraphHopper vs OSRM
Setting turn restrictions in GraphHopper vs OSRM requires fundamentally different configuration paradigms. GraphHopper handles turn costs natively through its Java routing core using a declarative turn_costs=true flag and standard OSM restriction relations, while OSRM relies on Lua profile scripts to parse restriction tags and apply penalties during the osrm-extract phase. Both engines consume OpenStreetMap data, but GraphHopper’s approach is configuration-driven and zero-code, whereas OSRM’s is script-driven and highly customizable at extraction time. For logistics engineers and Python backend developers, this means GraphHopper offers faster deployment with strict OSM compliance, while OSRM provides granular control over penalty scaling, vehicle-class filtering, and time-dependent routing logic.
| Feature | GraphHopper | OSRM |
|---|---|---|
| Restriction Processing | prepare phase (Java core) |
osrm-extract phase (Lua profile) |
| Configuration Style | Declarative (config.yml) |
Imperative (*.lua scripts) |
| OSM Compliance | Strict native mapping | Customizable via Lua |
| Penalty Control | Fixed weights or boolean blocks | Dynamic scaling & conditional logic |
| Rebuild Requirement | Only on PBF or config change | Required after Lua or PBF changes |
GraphHopper: Declarative, Configuration-Driven Routing
GraphHopper processes turn restrictions during the graph preparation phase. When graph.turn_costs=true is enabled in your configuration, the engine automatically parses OSM type=restriction relations and converts them into hard constraints or weighted penalties in the routing graph. Routing algorithms (A*, Dijkstra, or ALT) then respect these constraints natively without requiring custom preprocessing.
Configuration (config.yml):
graph.location: ./graph-cache
graph.datareader.file: region.osm.pbf
graph.turn_costs: true
profiles:
- name: car
vehicle: car
weighting: fastest
turn_costs: true
Python API Integration:
import requests
# Assumes GraphHopper server running on localhost:8989
url = "http://localhost:8989/route"
params = {
"point": ["52.51703,13.38886", "52.51821,13.39012"],
"profile": "car",
"turn_costs": "true",
"instructions": "true"
}
response = requests.get(url, params=params)
response.raise_for_status()
route = response.json()
print(f"Distance: {route['paths'][0]['distance']}m")
GraphHopper’s strength lies in its strict adherence to OSM standards. If your PBF contains valid restriction relations (with from, to, and via members), the engine maps them directly to turn-cost matrices. Understanding how these constraints map to the underlying topology is critical when debugging routing anomalies. The Handling Turn Restrictions in Routing Graphs guide breaks down how restriction relations translate into directed edge penalties and how missing via nodes cause silent graph fragmentation.
OSRM: Lua-Driven Extraction Pipeline
OSRM processes turn restrictions during osrm-extract via the Lua profile. The default car.lua profile includes a restrictions table that reads restriction tags and applies them as turn penalties or hard blocks. You can customize this by modifying the restrictions handler in your Lua script, enabling conditional logic based on vehicle type, time of day, or custom tags.
Lua Profile Snippet (car.lua):
restrictions = {
-- Default: parse standard OSM restriction relations
-- Custom logic can be injected here
process = function(restriction)
if restriction.type == "no_left_turn" then
return { penalty = 10000 } -- Hard block equivalent
end
return { penalty = 100 } -- Soft penalty
end
}
OSRM’s approach gives urban planners and backend developers fine-grained control over penalty scaling and conditional routing. Because restrictions are baked into the .osrm files during extraction, any Lua change requires a full osrm-extract and osrm-contract (or osrm-customize) rebuild. The OSRM Backend Restrictions Wiki provides authoritative documentation on Lua table structures and penalty weighting strategies.
Architecture & Performance Trade-offs
The core difference lies in when restrictions are evaluated. GraphHopper evaluates them at query time against a pre-built turn-cost matrix, which keeps memory overhead predictable but limits dynamic penalty adjustments without a server restart. OSRM evaluates them during extraction, baking penalties directly into edge weights. This yields faster query execution for static networks but increases build time and storage footprint.
For teams managing multi-modal fleets, OSRM’s Lua pipeline allows vehicle-class filtering (e.g., restriction:truck=no vs restriction:car=yes) by parsing custom OSM tags during extraction. GraphHopper handles this through profile-specific turn_costs configurations, but requires separate graph builds per vehicle class.
Data validation is equally critical. Malformed restriction relations (e.g., missing via nodes or incorrect from/to roles) cause silent routing failures in both engines. Referencing the OSM Graph Architecture & Network Modeling documentation helps engineers audit PBF files before ingestion and understand how directed edge weights propagate through contraction hierarchies.
Decision Matrix for Engineering Teams
| Scenario | Recommended Engine | Rationale |
|---|---|---|
| Rapid deployment, standard OSM data | GraphHopper | Zero-code config, strict OSM compliance, faster iteration |
| Custom penalty scaling, conditional logic | OSRM | Lua scripting enables dynamic, tag-driven restriction rules |
| Time-dependent routing (e.g., rush hour bans) | OSRM | osrm-customize supports time-sliced edge weights |
| Multi-tenant SaaS with isolated profiles | GraphHopper | Profile isolation and memory-efficient turn-cost matrices |
| Heavy debugging & topology auditing | GraphHopper | Transparent YAML config and Java stack traces |
Implementation Best Practices
- Validate OSM Relations First: Use
osmiumorpyosmiumto verifyrestrictionrelation topology before ingestion. Invalidviaroles are the #1 cause of silent turn-cost failures. - Benchmark Build Times: OSRM extraction scales linearly with Lua complexity. Profile your
restrictionstable before committing to production builds. - Cache Graphs Strategically: Both engines require disk-backed graph storage. Mount
/graph-cacheor/osrm-dataon NVMe volumes to reduce I/O bottlenecks during warm starts. - Monitor Penalty Drift: In logistics routing, a 500-second penalty mismatch can cascade into ETA inaccuracies. Log
turn_costsvspenaltyoutputs during QA to align with dispatch SLAs.
Choosing between these engines ultimately depends on your team’s tolerance for configuration complexity versus extraction overhead. GraphHopper excels when OSM compliance and rapid deployment are priorities, while OSRM dominates when restriction logic must adapt to proprietary fleet rules or municipal traffic ordinances.