ANN: OpenSourceRoutingMachine.jl — Julia wrapper for OSRM road routing

OpenSourceRoutingMachine.jl is a Julia wrapper for OSRM, a high-performance routing engine for road networks built on OpenStreetMap data.

Features

  • Graph — Build routing graphs from OpenStreetMap .pbf files (MLD or Contraction Hierarchies) with built-in car, bicycle, and foot profiles
  • Route — Shortest path with full geometry, turn-by-turn instructions, and per-edge annotations
  • Table — Distance/duration matrices between many-to-many points
  • Nearest — Snap coordinates to the nearest road network point
  • Match — Map-match noisy GPS traces to the road network
  • Trip — Solve the Traveling Salesman Problem over road networks
  • Tile — Retrieve road network geometry as vector tiles (MVT)

All query responses are returned as native Julia structs via FlatBuffers deserialization.

Installation

using Pkg
Pkg.add("OpenSourceRoutingMachine")

Example:

This example downloads an OpenStreetMap extract of Hamburg, builds a routing graph, computes a route from Hamburg Rathaus to the airport, and plots it on a map.

using Downloads
using OpenSourceRoutingMachine
using OpenSourceRoutingMachine.Graph
using OpenSourceRoutingMachine.Route

# 1. Download Hamburg OSM extract (~30 MB)
const OSM_FILE = joinpath(@__DIR__, "hamburg-latest.osm.pbf")
if !isfile(OSM_FILE)
    Downloads.download(
        "https://download.geofabrik.de/europe/germany/hamburg-latest.osm.pbf",
        OSM_FILE,
    )
end

# 2. Build an MLD routing graph (car profile)
const OSRM_BASE = joinpath(@__DIR__, "hamburg-latest.osrm")
extract(OSM_FILE; profile = PROFILE_CAR)
partition(OSRM_BASE)
customize(OSRM_BASE)

# 3. Create OSRM instance
osrm = OSRM(OSRM_BASE)

# 4. Route: Hamburg Rathaus → Hamburg Airport Terminal 1
params = RouteParams()
set_geometries!(params, GEOMETRIES_GEOJSON)
set_overview!(params, OVERVIEW_FULL)
add_coordinate!(params, Position(9.9937, 53.5511))    # Rathaus
add_coordinate!(params, Position(10.0055, 53.6303))   # Airport

response = route(osrm, params)
r = response.routes[1]

println("Distance: $(round(r.distance / 1000; digits = 1)) km")
println("Duration: $(round(r.duration / 60; digits = 1)) min")

# 5. Plot the route on an OpenStreetMap background
using CairoMakie
using Tyler
using Tyler.MapTiles
using Tyler.TileProviders

# Extract route coordinates and project to Web Mercator
lons = [c.longitude for c in r.coordinates]
lats = [c.latitude  for c in r.coordinates]

to_webmerc(lon, lat) = Point2f(MapTiles.project((lon, lat), MapTiles.wgs84, MapTiles.web_mercator))
points = [to_webmerc(lon, lat) for (lon, lat) in zip(lons, lats)]

# Map extent with padding
pad = 0.02
extent = Rect2f(
    minimum(lons) - pad, minimum(lats) - pad,
    maximum(lons) - minimum(lons) + 2pad,
    maximum(lats) - minimum(lats) + 2pad,
)

m = Tyler.Map(extent; provider = TileProviders.OpenStreetMap(:Mapnik))
wait(m)

# Draw route and markers
lines!(m.axis, points; color = :dodgerblue, linewidth = 4)
scatter!(
    m.axis, [points[1], points[end]];
    color = [:green, :red], markersize = 16,
)

display(m.figure)

The output:

Links

Cool, thanks.
However the name is a bit odd.. as it is a wrapper of OSRM, why not OSRM.jl ?

How much data has to be downloaded? Is that per city, or per country, or how can I limit the amount of data that needs to be downloaded?

I am trying to replicate your example, but I got a strange error on extract(OSM_FILE; profile = PROFILE_CAR) :

[2026-04-28T07:57:22.504749642] [error] the argument ('/home/lobianco/CloudFiles/beta-lorraine-sync/Documents/Teaching/2026-2027/Model-assisted prospective for forests and forest sectors/Model-assisted prospective for forests and forest sectors/part2 - programing/gis/hamburg-latest.osm.pbf') for option '--input' is invalid
ERROR: failed process: Process(setenv(`/home/lobianco/.julia/artifacts/56ae55afea5a04e956cefa04ff3b50c6c4cd81ee/bin/osrm-extract --profile /home/lobianco/.julia/artifacts/56ae55afea5a04e956cefa04ff3b50c6c4cd81ee/profiles/car.lua '/home/lobianco/CloudFiles/beta-lorraine-sync/Documents/Teaching/2026-2027/Model-assisted prospective for forests and forest sectors/Model-assisted prospective for forests and forest sectors/part2 - programing/gis/hamburg-latest.osm.pbf'`,["QT_ACCESSIBILITY=1", "LD_LIBRARY_PATH=/home/lobianco/.julia/juliaup/julia-1.12.6+0.x64.linux.gnu/bin/../lib/julia:/home/lobianco/.julia/artifacts/715b660f53eb83c33e199a44ececfd8dc03f2a27/lib:/home/lobianco/.julia/artifacts/3d5bf8a817e520b6422a0630700ccac87eb57e9e/lib:/home/lobianco/.julia/artifacts/ef00fd79126cf3faac9fbce599a27077b6b8e958/lib:/home/lobianco/.julia/artifacts/b2615156eadd07cea1a8d9c5b02ebe9bb2ef1f3b/lib:/home/lobianco/.julia/artifacts/13739997a0df2e9b7aa44a748af88a492e54b324/lib:/home/lobianco/.julia/artifacts/9723cba3628566f8da50ea2f286daaf70c23a7a2/lib:/home/lobianco/.julia/artifacts/25473fbae0018ebeb1b5a6aa9037afe58e763619/lib:/home/lobianco/.julia/juliaup/julia-1.12.6+0.x64.linux.gnu/bin/../lib/julia:/home/lobianco/.julia/juliaup/julia-1.12.6+0.x64.linux.gnu/bin/../lib", "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus,guid=3fa5c049ff30ea7a5264af5c69ef7e30", "TERMINFO_DIRS=/home/lobianco/.julia/artifacts/ef00fd79126cf3faac9fbce599a27077b6b8e958/share/terminfo", "FC_FONTATIONS=1", "DBUS_STARTER_ADDRESS=unix:path=/run/user/1000/bus,guid=3fa5c049ff30ea7a5264af5c69ef7e30", "SYSTEMD_EXEC_PID=35765", "XDG_SESSION_TYPE=wayland", "USER=lobianco", "XDG_CONFIG_DIRS=/etc/xdg/xdg-ubuntu:/etc/xdg"  …  "DESKTOP_SESSION=ubuntu", "CHROME_DESKTOP=code.desktop", "_=/usr/share/code/code", "MEMORY_PRESSURE_WRITE=c29tZSAyMDAwMDAgMjAwMDAwMAA=", "XDG_SESSION_CLASS=user", "TERM_PROGRAM=vscode", "GNOME_SETUP_DISPLAY=unix:/tmp/.X11-unix/X1", "VSCODE_GIT_ASKPASS_MAIN=/usr/share/code/resources/app/extensions/git/dist/askpass-main.js", "GIO_LAUNCHED_DESKTOP_FILE_PID=68227", "LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=00:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.avif=01;35:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:*~=00;90:*#=00;90:*.bak=00;90:*.old=00;90:*.orig=00;90:*.part=00;90:*.rej=00;90:*.swp=00;90:*.tmp=00;90:*.dpkg-dist=00;90:*.dpkg-old=00;90:*.ucf-dist=00;90:*.ucf-new=00;90:*.ucf-old=00;90:*.rpmnew=00;90:*.rpmorig=00;90:*.rpmsave=00;90:"]), ProcessExited(1)) [1]

Stacktrace:
 [1] pipeline_error
   @ ./process.jl:597 [inlined]
 [2] run(::Cmd; wait::Bool)
   @ Base ./process.jl:512
 [3] run
   @ ./process.jl:509 [inlined]
 [4] extract(osm_path::String; profile::Profile, verbosity::Verbosity, data_version::String, threads::Nothing, small_component_size::Int64, with_osm_metadata::Bool, parse_conditional_restrictions::Bool, location_dependent_data::Vector{…}, disable_location_cache::Bool, dump_nbg_graph::Bool)
   @ OpenSourceRoutingMachine.Graph ~/.julia/packages/OpenSourceRoutingMachine/h92XX/src/graph/extract.jl:102
 [5] top-level scope
   @ ~/CloudFiles/beta-lorraine-sync/Documents/Teaching/2026-2027/Model-assisted prospective for forests and forest sectors/Model-assisted prospective for forests and forest sectors/part2 - programing/gis/OSM.jl:21
Some type information was truncated. Use `show(err)` to see complete types.

Created an issue on the repository. Just quote the input parameter (path of the file) :slight_smile:

At @sylvaticus thanks for trying this out. I will take a look into the issue in the next couple of days. I think naming things is hard, and one will never please everyone.

@ufechner7 you can choose a region that fits your needs. In principle, you could take the whole world if you had a big enough machine. However, if you just want to route within a city, this is a waste of time and resources. See e.g. https://download.geofabrik.de/ for selecting some region.

Concerning package name, see also here: 5. Creating Packages · Pkg.jl

I made a PR to address the issue. This seems to be a whitespace issue in OSRM paths. If you find time to confirm that this resolves the issue would be great.

Hello, yes thank you, the latest release solve the path issue on my Linux machine.
I am not sure why you introduced a wrapper function with temporary symlinks and how much resilient is this solution on the various OSs.. why you didn’t simply wrap the path string in quotes ?

Julia’s run() doesn’t go through a shell — it passes arguments directly via execve /argv , so quoting has no effect. I verified this: a test C program receives the full path correctly as a single argv[1] . The bug is inside OSRM’s Boost.ProgramOptions parser, which rejects positional arguments containing spaces even when delivered correctly at the OS level. Since we can’t fix OSRM’s argument parsing quickly, the symlink into a temp directory (guaranteed space-free by the OS on all platforms) is the simplest reliable workaround I could come up with.

hey! that looks nice! Did you try to run it at scale? I have an application in R where I need to compute 50K distances using OSRM::osrmTable, but for that to work I need to run their server in a local docker and make osrm query that over localhost. Can you avoid that server issue completely? how?

osrm-backend can be used via a HTTP API and as C++ library. In Julia in general, there is no C++ interface, but one can do it via a C wrapper that we developed; see libosrmc. This way we can use OSRM without the HTTP overhead.

OpenSourceRoutingMachine.jl can be used to derive large distance/duration matrices; you might need to change some config with a setter on the OSRM object to expand the restrictions similar to the HTTP API. The default values are unchanged from vanilla osrm-backen.

Ok great. Just to be sure I’m getting this all correctly, you are using HTTP but the query runs locally- not sending stuff over internet. Given your example about preparing the map data locally that seems pretty clear but I want to make sure I get this right. My use case is most of the time getting a huge number of distances.

For me that’s a really cool addition to the toolkit!

No, we don’t use HTTP, we use the OSRM library. If you call something like table(osrm, params) this calls internally the libosrmc_jll which calls osrm_jll (the OSRM library built with BinaryBuilder.jl). So we don’t have an OSRM server, no Docker, nothing of that, just plain library calls.

Awesome . Exactly what I was hoping for. Will give a whirl. Thanks

Hello, how did you solve the symlinks troubles on Windows? On it one needs to have an administrator account to be able to create symbolic links. And even with an administrator account, be able to create symlinks is not activated by default.

I don’t know much about Windows and rely heavily on the CI for it to work. If you encounter a problem, I would appreciate it if you could open an issue with a reproducer that I can use in a test, or even better, a PR if possible. I have opened a PR at OSRM to work around the Boost issue. Hopefully we can remove the workaround in the near future again.

Thanks, I I have not installed your package but was curious if some trick allowed to overcome the so idiot difficulties imposed by Microsoft to create symbolic links.