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

  1. Validate OSM Relations First: Use osmium or pyosmium to verify restriction relation topology before ingestion. Invalid via roles are the #1 cause of silent turn-cost failures.
  2. Benchmark Build Times: OSRM extraction scales linearly with Lua complexity. Profile your restrictions table before committing to production builds.
  3. Cache Graphs Strategically: Both engines require disk-backed graph storage. Mount /graph-cache or /osrm-data on NVMe volumes to reduce I/O bottlenecks during warm starts.
  4. Monitor Penalty Drift: In logistics routing, a 500-second penalty mismatch can cascade into ETA inaccuracies. Log turn_costs vs penalty outputs 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.