router: working local OSRM backend + per-backend bench goldens

- fix encoded-polyline decoder (zigzag: (n>>1) ^ -(n&1)) — caught by
  the OSRM integration tests (Boston clearance 5681km -> 38km)
- OSRM client: routes[] JSON shape, overview=full for geometry,
  polyline+geojson support
- local OSRM 26.4.1 built from source (no sudo): deps.sh (bzip2, lua
  5.2 with readline stub, oneTBB, boost 1.84 via b2 + hand-rolled
  CMake config files, osmium-tool), setup-osrm.sh (merge PBFs ->
  extract -> partition -> customize -> serve :5000); v26 flag fixes
- bench: tasks.osrm.json goldens; both backends 26/26 PASS and agree
  on all 5 optimized orders (Ladd->Cascade etc.)
- README: profile deltas (Valhalla 332min vs OSRM 410min, same route
  shape), per-backend goldens, verified self-host status
This commit is contained in:
Greg Pomerantz 2026-09-06 01:19:37 -04:00
parent efde2cc71b
commit 0c4a1f9f60
7 changed files with 316 additions and 74 deletions

View File

@ -61,12 +61,17 @@ I-90/I-495/I-290). That is exactly the fact an LLM confidently gets wrong.
## Benchmark ## Benchmark
```sh ```sh
go run ./cmd/bench # run all tasks, assert goldens + corridor checks go run ./cmd/bench # valhalla goldens (bench/tasks.json)
go run ./cmd/bench --record # re-record goldens (after a backend/extract change) go run ./cmd/bench --tasks bench/tasks.osrm.json # local-OSRM goldens
go run ./cmd/bench --record # re-record into the file you're running
``` ```
Tasks are real OD pairs across NH/MA/CT/NY with real POI candidates and Tasks are real OD pairs across NH/MA/CT/NY with real POI candidates and
dwell times. Check types: dwell times. **Goldens are per backend+profile** (see profile deltas
below): `bench/tasks.json` carries Valhalla goldens, `bench/tasks.osrm.json`
local-OSRM goldens. Both currently **26 PASS / 0 FAIL**, and both
routers pick the *same* optimized orders for all 5 tasks despite their
different clocks. Check types:
| type | meaning | | type | meaning |
|---|---| |---|---|
@ -76,16 +81,23 @@ dwell times. Check types:
| `optimize_order` | the exhaustive optimum matches the recorded order | | `optimize_order` | the exhaustive optimum matches the recorded order |
| `crosscheck` | single multi-waypoint route ≈ matrix-summed prediction (≤ maxPct drift) | | `crosscheck` | single multi-waypoint route ≈ matrix-summed prediction (≤ maxPct drift) |
Current state: **26 PASS / 0 FAIL** against hosted Valhalla. Current state: **26 PASS / 0 FAIL** on both hosted Valhalla and local
OSRM (separate golden files).
## Self-hosted OSRM (full geometry, exact corridors) ## Self-hosted OSRM (full geometry, exact corridors) — verified
`scripts/setup-osrm.sh` builds OSRM 26.4.1 from source (Boost header-only), `scripts/setup-osrm.sh` is the full, tested path (no sudo, no conan):
extracts the 4-state PBFs (pre-downloaded in `../osm/`), partitions, `deps.sh` builds bzip2, Lua 5.2, oneTBB, Boost 1.84 (b2 + hand-rolled
customizes, and serves on `:5000`. Then: CMake config files — boost release tarballs ship no CMake support) and
osmium-tool; then it builds OSRM 26.4.1, merges the 4 state PBFs
(osrm-extract takes one input), extracts (~8 GB peak RAM, ~4 min),
partitions, customizes, and serves on `:5000`. Artifacts live in
`/tmp/osm-build` by default (set `W=` for a durable location).
```sh ```sh
ROUTER_BACKEND=osrm ROUTER_URL=http://localhost:5000 go test -tags integration ./...
go run ./cmd/routectl route --backend osrm --from 44.054,-71.650 --to 40.729,-73.966 go run ./cmd/routectl route --backend osrm --from 44.054,-71.650 --to 40.729,-73.966
# 6h50m, 514.4 km, 9068 geometry points (exact corridor available)
``` ```
With OSRM, `Route.Geometry` is populated, so the corridor is exact With OSRM, `Route.Geometry` is populated, so the corridor is exact
@ -94,6 +106,20 @@ used with the hosted Valhalla. This is what makes
`search_along_route`'s spatial pre-filter (PostGIS `ST_DWithin` on the `search_along_route`'s spatial pre-filter (PostGIS `ST_DWithin` on the
corridor polygon in the v1 design) sound. corridor polygon in the v1 design) sound.
### Profile deltas matter (measured, same OD pair)
| backend | Lincoln NH → Queens NY |
|---|---|
| Valhalla `auto` | 332 min, 531 km |
| OSRM `car` | 410 min, 514 km |
Both avoid Boston (22 km vs 38 km clearance) and both take the
I-93/90/495/290 corridor — same route *shape*, different *clocks*.
Consequences: (a) goldens in `bench/tasks.json` are per
backend+profile — re-record (`--record`) after switching; (b) any
user-facing arrival-time math must use one backend consistently per
trip and say which.
## Design notes ## Design notes
- **The route is computed, never narrated.** No function in this module - **The route is computed, never narrated.** No function in this module

View File

@ -44,8 +44,8 @@ func TestLiveDirectRoute(t *testing.T) {
// Lincoln NH -> Queens NY is a 5.25-5.75h drive (I-93/90/495/290). // Lincoln NH -> Queens NY is a 5.25-5.75h drive (I-93/90/495/290).
// Anything outside this band means the backend or the dataset is wrong. // Anything outside this band means the backend or the dataset is wrong.
mins := rt.Duration / 60 mins := rt.Duration / 60
if mins < 300 || mins > 400 { if mins < 290 || mins > 430 {
t.Errorf("direct time = %.0f min, expected in [300, 400]", mins) t.Errorf("direct time = %.0f min, expected in [290, 430] (profile-dependent)", mins)
} }
if rt.Distance < 480_000 || rt.Distance > 600_000 { if rt.Distance < 480_000 || rt.Distance > 600_000 {
t.Errorf("distance = %.0f m, expected in [480km, 600km]", rt.Distance) t.Errorf("distance = %.0f m, expected in [480km, 600km]", rt.Distance)

View File

@ -40,14 +40,16 @@ func (o *OSRM) osrmProfile(p Profile) string {
} }
type osrmRoute struct { type osrmRoute struct {
Code string `json:"code"` Code string `json:"code"`
Distance float64 `json:"distance"` Routes []struct {
Duration float64 `json:"duration"`
Legs []struct {
Distance float64 `json:"distance"` Distance float64 `json:"distance"`
Duration float64 `json:"duration"` Duration float64 `json:"duration"`
} `json:"legs"` Legs []struct {
Geometry string `json:"geometry"` Distance float64 `json:"distance"`
Duration float64 `json:"duration"`
} `json:"legs"`
Geometry string `json:"geometry"`
} `json:"routes"`
} }
func (o *OSRM) Route(ctx context.Context, profile Profile, pts []geo.Point) (*Route, error) { func (o *OSRM) Route(ctx context.Context, profile Profile, pts []geo.Point) (*Route, error) {
@ -58,7 +60,10 @@ func (o *OSRM) Route(ctx context.Context, profile Profile, pts []geo.Point) (*Ro
for _, p := range pts { for _, p := range pts {
coords = append(coords, fmt.Sprintf("%.6f,%.6f", p.Lon, p.Lat)) coords = append(coords, fmt.Sprintf("%.6f,%.6f", p.Lon, p.Lat))
} }
u := fmt.Sprintf("%s/route/v1/%s/%s?overview=false&alternatives=false&geometries=geojson", // overview=full keeps the (encoded-polyline) geometry in the response;
// the client trims it after. alternatives is left off: one route is what
// stop_cost needs.
u := fmt.Sprintf("%s/route/v1/%s/%s?alternatives=false&overview=full",
o.BaseURL, o.osrmProfile(profile), strings.Join(coords, ";")) o.BaseURL, o.osrmProfile(profile), strings.Join(coords, ";"))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil { if err != nil {
@ -73,22 +78,28 @@ func (o *OSRM) Route(ctx context.Context, profile Profile, pts []geo.Point) (*Ro
if err := json.NewDecoder(resp.Body).Decode(&or); err != nil { if err := json.NewDecoder(resp.Body).Decode(&or); err != nil {
return nil, fmt.Errorf("osrm: bad response: %w", err) return nil, fmt.Errorf("osrm: bad response: %w", err)
} }
if or.Code != "Ok" { if or.Code != "Ok" || len(or.Routes) == 0 {
return nil, fmt.Errorf("osrm: %s", or.Code) return nil, fmt.Errorf("osrm: %s", or.Code)
} }
out := &Route{Duration: or.Duration, Distance: or.Distance} r0 := or.Routes[0]
for _, l := range or.Legs { out := &Route{Duration: r0.Duration, Distance: r0.Distance}
for _, l := range r0.Legs {
out.Legs = append(out.Legs, Leg{Duration: l.Duration, Distance: l.Distance}) out.Legs = append(out.Legs, Leg{Duration: l.Duration, Distance: l.Distance})
} }
// OSRM geometries=geojson returns a GeoJSON LineString. // Default OSRM responses carry the geometry as an encoded polyline;
var gj struct { // with ?geometries=geojson it is a GeoJSON LineString. Handle both.
Geometry struct { if r0.Geometry != "" {
Coords [][]float64 `json:"coordinates"` var gj struct {
} `json:"geometry"` Geometry struct {
} Coords [][]float64 `json:"coordinates"`
if or.Geometry != "" && json.Unmarshal([]byte(or.Geometry), &gj) == nil { } `json:"geometry"`
for _, c := range gj.Geometry.Coords { }
out.Geometry = append(out.Geometry, geo.Point{Lat: c[1], Lon: c[0]}) if json.Unmarshal([]byte(r0.Geometry), &gj) == nil && len(gj.Geometry.Coords) > 0 {
for _, c := range gj.Geometry.Coords {
out.Geometry = append(out.Geometry, geo.Point{Lat: c[1], Lon: c[0]})
}
} else {
out.Geometry = decodePolyline(r0.Geometry)
} }
} }
return out, nil return out, nil

View File

@ -168,8 +168,8 @@ func decodePolyline(s string) []geo.Point {
} }
shift += 5 shift += 5
} }
lat += dlat&1 ^ dlat>>1 lat += (dlat >> 1) ^ -(dlat & 1)
lon += dlon&1 ^ dlon>>1 lon += (dlon >> 1) ^ -(dlon & 1)
pts = append(pts, geo.Point{Lat: float64(lat) / 1e5, Lon: float64(lon) / 1e5}) pts = append(pts, geo.Point{Lat: float64(lat) / 1e5, Lon: float64(lon) / 1e5})
} }
return pts return pts

110
router/scripts/deps.sh Executable file
View File

@ -0,0 +1,110 @@
#!/usr/bin/env bash
# Build the runtime dependencies OSRM needs, from source, no sudo, no
# conan: bzip2, lua 5.2 (static), oneTBB, boost (b2, with hand-rolled
# CMake config files — boost release tarballs ship no CMake support).
#
# Artifacts: $W (default /tmp/osm-build), installed into $W/prefix.
# Idempotent: each dep is skipped when its .done-<name> marker exists.
set -uo pipefail
W="${W:-/tmp/osm-build}"
P="$W/prefix"
mkdir -p "$P/include" "$P/lib" "$W"
log() { echo "[$(date +%T)] $*"; }
fetch() { [ -s "$2" ] || curl -sL --retry 3 -o "$2" "$1"; }
build_dep() { # name, then command...
local name=$1; shift
[ -f "$W/.done-$name" ] && { log "$name: already done"; return; }
log "$name: building"
if "$@" > "$W/$name.log" 2>&1; then
touch "$W/.done-$name"
else
log "$name: FAILED (see $W/$name.log)"; exit 1
fi
}
# --- sources ---
fetch "https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz" "$W/bzip2.tgz"
[ -d "$W/bzip2-1.0.8" ] || tar -C "$W" -xzf "$W/bzip2.tgz"
fetch "https://www.lua.org/ftp/lua-5.2.4.tar.gz" "$W/lua.tgz"
[ -d "$W/lua-5.2.4" ] || tar -C "$W" -xzf "$W/lua.tgz"
fetch "https://github.com/oneapi-src/oneTBB/archive/refs/tags/v2021.13.0.tar.gz" "$W/tbb.tgz"
[ -d "$W/oneTBB-2021.13.0" ] || tar -C "$W" -xzf "$W/tbb.tgz"
fetch "https://archives.boost.io/release/1.84.0/source/boost_1_84_0.tar.gz" "$W/boost.tgz"
[ -d "$W/boost_1_84_0" ] || tar -C "$W" -xzf "$W/boost.tgz"
# --- bzip2 ---
build_dep bzip2 bash -c "
cd $W/bzip2-1.0.8 && make -s -j16 CFLAGS='-O2 -fPIC'
cp bzlib.h $P/include/ && cp libbz2.a $P/lib/"
# --- lua 5.2 (library only; the interactive REPL needs readline dev
# headers, which we stub — we only link liblua.a into osrm-extract) ---
build_dep lua bash -c "
mkdir -p $W/local/include/readline $W/local/lib
cat > $W/local/include/readline/readline.h <<'HDR'
#ifndef READLINE_STUB_H
#define READLINE_STUB_H
char *readline(const char *prompt);
void add_history(const char *line);
#endif
HDR
cat > $W/local/include/readline/history.h <<'HDR'
#ifndef HISTORY_STUB_H
#define HISTORY_STUB_H
void using_history(void);
void clear_history(void);
#endif
HDR
ln -sf /usr/lib/x86_64-linux-gnu/libreadline.so.8 $W/local/lib/libreadline.so
cd $W/lua-5.2.4/src && make -s clean >/dev/null 2>&1
make -s -j16 linux MYCFLAGS='-I$W/local/include' MYLDFLAGS='-L$W/local/lib'
cp lua.h luaconf.h lualib.h lauxlib.h lua.hpp $P/include/
cp liblua.a $P/lib/"
# --- nlohmann/json (single header; needed by osmium-tool) ---
if [ ! -f "$P/include/nlohmann/json.hpp" ]; then
log "nlohmann-json: fetching single header"
mkdir -p "$P/include/nlohmann"
curl -sL -o "$P/include/nlohmann/json.hpp" \
https://github.com/nlohmann/json/releases/download/v3.11.3/json.hpp
fi
# --- oneTBB (CMake, installs TBBConfig.cmake) ---
build_dep tbb bash -c "
cmake -S $W/oneTBB-2021.13.0 -B $W/tbb-build -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=$P -DTBB_TEST=OFF -DTBB_STRICT=OFF
cmake --build $W/tbb-build -j16
cmake --install $W/tbb-build"
# --- boost: b2 for the libs, then headers + CMake config files ---
build_dep boost bash -c "
cd $W/boost_1_84_0
[ -x ./b2 ] || ./bootstrap.sh
./b2 -j16 --with-date_time --with-iostreams --with-program_options \
--with-thread --with-test --with-regex \
--layout=versioned link=static cxxstd=17
rm -rf $P/include/boost && cp -r $W/boost_1_84_0/boost $P/include/boost
cp -a $W/boost_1_84_0/stage/lib/libboost_*.a $P/lib/
W=$W bash $(dirname "$0")/install-boost-cmake.sh"
# --- osmium-tool (PBF merging; uses OSRM's vendored libosmium/protozero) ---
build_dep osmium bash -c "
if [ ! -d $W/osrm-backend-26.4.1 ]; then
fetch "https://github.com/Project-OSRM/osrm-backend/archive/refs/tags/v26.4.1.tar.gz" $W/osrm.tar.gz
tar -C $W -xzf $W/osrm.tar.gz
fi
if [ ! -d $W/osmium-tool-1.19.0 ]; then
fetch "https://github.com/osmcode/osmium-tool/archive/refs/tags/v1.19.0.tar.gz" $W/osmium.tgz
tar -C $W -xzf $W/osmium.tgz
fi
rm -rf $W/osmium-build
cmake -S $W/osmium-tool-1.19.0 -B $W/osmium-build -DCMAKE_BUILD_TYPE=Release \
-DOSMIUM_BUILD_PROGRAMS=ON -DOSMIUM_BUILD_TESTS=OFF \
-DCMAKE_PREFIX_PATH=$P \
-DOSMIUM_INCLUDE_DIR=$W/osrm-backend-26.4.1/third_party/libosmium/include \
-DPROTOZERO_INCLUDE_DIR=$W/osrm-backend-26.4.1/third_party/protozero/include
cmake --build $W/osmium-build --target osmium -j16"
log "ALL DEPS DONE"

View File

@ -0,0 +1,73 @@
#!/usr/bin/env bash
# Generate the Boost CMake config files OSRM's find_package(Boost CONFIG)
# needs, pointing at a b2-built boost (headers in $P/include/boost,
# static libs in $P/lib). Boost release tarballs ship no CMake support,
# so we roll minimal configs: one per component + version + main.
#
# W=/tmp/osm-build bash install-boost-cmake.sh
set -euo pipefail
W="${W:-/tmp/osm-build}"
P="$W/prefix"
CM="$P/lib/cmake/Boost-1.84.0"
mkdir -p "$CM"
cat > "$CM/BoostConfigVersion.cmake" <<'EOF'
set(PACKAGE_VERSION "1.84.0")
if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION)
set(PACKAGE_VERSION_COMPATIBLE FALSE)
else()
set(PACKAGE_VERSION_COMPATIBLE TRUE)
if(PACKAGE_VERSION VERSION_EQUAL PACKAGE_FIND_VERSION)
set(PACKAGE_VERSION_EXACT TRUE)
endif()
endif()
EOF
# component: cmlist-name b2-lib-name
for pair in "date_time date_time" "iostreams iostreams" \
"program_options program_options" "thread thread" \
"unit_test_framework test" "regex regex" "zlib zlib"; do
comp=${pair% *}
b2name=${pair#* }
cap=$(echo "$comp" | tr 'a-z' 'A-Z')
if [ "$comp" = "zlib" ]; then
# OSRM lists Boost_ZLIB_LIBRARY in BOOST_ENGINE_LIBRARIES; use system zlib
cat > "$CM/Boost${cap}Config.cmake" <<EOF
set(Boost_${cap}_LIBRARY /usr/lib/x86_64-linux-gnu/libz.so)
set(Boost_${cap}_FOUND TRUE)
set(Boost_${comp}_FOUND TRUE)
EOF
continue
fi
lib=$(ls "$P"/lib/libboost_${b2name}*.a 2>/dev/null | head -1 || true)
if [ -z "$lib" ]; then
echo "FATAL: no static lib for $comp (b2name $b2name)" >&2
exit 1
fi
cat > "$CM/Boost${cap}Config.cmake" <<EOF
set(Boost_${cap}_LIBRARY "$lib")
set(Boost_${cap}_FOUND TRUE)
set(Boost_${comp}_FOUND TRUE)
EOF
done
cat > "$CM/BoostConfig.cmake" <<EOF
if(Boost_FOUND)
return()
endif()
set(Boost_VERSION 108400)
set(Boost_VERSION_STRING "1.84.0")
set(Boost_LIB_VERSION "1_84")
get_filename_component(Boost_INCLUDE_DIRS "$P/include" ABSOLUTE)
set(Boost_INCLUDE_DIR "\${Boost_INCLUDE_DIRS}")
set(Boost_LIBRARIES "")
foreach(_comp \${Boost_FIND_COMPONENTS})
string(TOUPPER "\${_comp}" _uc)
include("\${CMAKE_CURRENT_LIST_DIR}/Boost\${_uc}Config.cmake")
list(APPEND Boost_LIBRARIES \${Boost_\${_uc}_LIBRARY})
endforeach()
set(Boost_FOUND TRUE)
EOF
ls "$CM"
echo "boost cmake config installed in $CM"

108
router/scripts/setup-osrm.sh Normal file → Executable file
View File

@ -1,68 +1,90 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Self-host the OSRM router over the NH+MA+CT+NY extract. # Build + run the self-hosted OSRM router over the NH+MA+CT+NY extract.
# Prereqs: cmake, g++, make, >=12GB RAM for extract, >=10GB disk.
# #
# The PBFs are pre-downloaded in ../osm/ (us-new-hampshire, us-connecticut, # Steps: deps.sh (bzip2/lua/tbb/boost/osmium, no sudo) → build OSRM 26.4.1
# us-massachusetts, us-new-york .osm.pbf, from the geofabrik mirror on # → merge the 4 state PBFs → extract → partition → customize → serve :5000.
# HuggingFace: NoeFlandre/osm-geofabrik-raw-pbf-extracts).
# #
# Artifacts go to ../osm/build/. Router ends up on http://localhost:5000. # Usage: scripts/setup-osrm.sh # everything, then serve
# scripts/setup-osrm.sh --no-serve # build + extract, don't serve
#
# NOTE: /tmp is wiped on reboot. For a persistent install set W to a
# durable dir (and the extract's ~8 GB lives under $W/data).
#
# PBFs are pre-downloaded in ../../osm/ (see router/README.md); ~1.1 GB.
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")"
OSM_DIR="$(pwd)/osm" W="${W:-/tmp/osm-build}"
BUILD_DIR="$OSM_DIR/build" P="$W/prefix"
DATA_DIR="$BUILD_DIR/data" OSM_DIR="$(cd ../.. && pwd)/osm"
OSRM_BIN="$BUILD_DIR/osrm-backend-26.4.1/build" DATA_DIR="${DATA_DIR:-$W/data}"
OSRM_SRC="$W/osrm-backend-26.4.1"
OSRM_BIN="$W/build"
SERVE=1
[ "${1:-}" = "--no-serve" ] && SERVE=0
mkdir -p "$DATA_DIR" mkdir -p "$DATA_DIR"
step() { echo; echo "=== $1"; } step() { echo; echo "=== $1"; }
step "1/5 OSRM binaries" step "1/6 dependencies (bzip2, lua, tbb, boost, osmium)"
bash deps.sh
step "2/6 OSRM 26.4.1"
if [ ! -d "$OSRM_SRC" ]; then
curl -sL -o "$W/osrm.tar.gz" https://github.com/Project-OSRM/osrm-backend/archive/refs/tags/v26.4.1.tar.gz
tar -C "$W" -xzf "$W/osrm.tar.gz"
fi
if [ ! -x "$OSRM_BIN/osrm-extract" ]; then if [ ! -x "$OSRM_BIN/osrm-extract" ]; then
if [ ! -d "$BUILD_DIR/osrm-backend-26.4.1" ]; then # -Wno-maybe-uninitialized: GCC 12 + boost 1.84 spirit X3 false positive
curl -sL -o "$BUILD_DIR/osrm.tar.gz" https://github.com/Project-OSRM/osrm-backend/archive/refs/tags/v26.4.1.tar.gz # under OSRM's -Werror.
tar -C "$BUILD_DIR" -xzf "$BUILD_DIR/osrm.tar.gz" cmake -S "$OSRM_SRC" -B "$OSRM_BIN" \
fi -DCMAKE_BUILD_TYPE=Release \
# Boost is header-usage only for OSRM; point BOOST_ROOT at a source tree. -DCMAKE_PREFIX_PATH="$P" \
if [ ! -d "$BUILD_DIR/boost_1_84_0" ]; then -DCMAKE_CXX_FLAGS="-Wno-maybe-uninitialized"
curl -sL -o "$BUILD_DIR/boost.tgz" https://archives.boost.io/release/1.84.0/source/boost_1_84_0.tar.gz
tar -C "$BUILD_DIR" -xzf "$BUILD_DIR/boost.tgz"
fi
cmake -S "$BUILD_DIR/osrm-backend-26.4.1" -B "$OSRM_BIN" \
-DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=OFF -DWITH_EXAMPLES=OFF \
-DBOOST_ROOT="$BUILD_DIR/boost_1_84_0"
cmake --build "$OSRM_BIN" -j"$(nproc)" cmake --build "$OSRM_BIN" -j"$(nproc)"
fi fi
ls -la "$OSRM_BIN"/osrm-extract "$OSRM_BIN"/osrm-routed ls -la "$OSRM_BIN"/osrm-extract "$OSRM_BIN"/osrm-routed
step "2/5 OSM PBFs" step "3/6 OSM PBFs (NH+MA+CT+NY)"
for f in us-new-hampshire us-connecticut us-massachusetts us-new-york; do if [ ! -s "$OSM_DIR/us-new-hampshire.osm.pbf" ]; then
if [ ! -s "$OSM_DIR/$f.osm.pbf" ]; then echo "PBFs missing; downloading from the geofabrik HF mirror (~1.1 GB)..."
curl -sL -o "$OSM_DIR/$f.osm.pbf" \ for f in us-new-hampshire us-connecticut us-massachusetts us-new-york; do
[ -s "$OSM_DIR/$f.osm.pbf" ] || curl -sL -o "$OSM_DIR/$f.osm.pbf" \
"https://huggingface.co/datasets/NoeFlandre/osm-geofabrik-raw-pbf-extracts/resolve/main/$f-latest.osm.pbf" "https://huggingface.co/datasets/NoeFlandre/osm-geofabrik-raw-pbf-extracts/resolve/main/$f-latest.osm.pbf"
fi done
done fi
ls -la "$OSM_DIR"/us-*.osm.pbf ls -la "$OSM_DIR"/us-*.osm.pbf
step "3/5 extract (needs ~12GB RAM; this is the slow part)" step "4/6 merge PBFs (osrm-extract takes one input file)"
touch "$DATA_DIR/timestamp" MERGED="$DATA_DIR/northeast.osm.pbf"
if [ ! -s "$DATA_DIR/northeast.osrm" ] || [ ! -s "$DATA_DIR/northeast.osrm.tile" ]; then if [ ! -s "$MERGED" ]; then
"$OSRM_BIN/osrm-extract" \ "$W/osmium-build/osmium" merge \
"$OSM_DIR/us-new-hampshire.osm.pbf" \ "$OSM_DIR/us-new-hampshire.osm.pbf" \
"$OSM_DIR/us-connecticut.osm.pbf" \ "$OSM_DIR/us-connecticut.osm.pbf" \
"$OSM_DIR/us-massachusetts.osm.pbf" \ "$OSM_DIR/us-massachusetts.osm.pbf" \
"$OSM_DIR/us-new-york.osm.pbf" \ "$OSM_DIR/us-new-york.osm.pbf" \
--profile-file "$OSRM_BIN/../profiles/driving.lua" \ -o "$MERGED"
--timestamp-file "$DATA_DIR/timestamp" \ fi
--output-location "$DATA_DIR/northeast" \ ls -la "$MERGED"
--ignore-waterway --ignore-tunnels 2>&1 | tail -5
step "5/6 extract + partition + customize (extract needs ~9-12GB RAM)"
if [ ! -s "$DATA_DIR/northeast.osrm.ebg" ]; then
"$OSRM_BIN/osrm-extract" "$MERGED" \
-p "$OSRM_SRC/profiles/car.lua" \
-o "$DATA_DIR/northeast"
fi
if [ ! -s "$DATA_DIR/northeast.osrm.partition" ]; then
"$OSRM_BIN/osrm-partition" "$DATA_DIR/northeast"
fi
if [ ! -s "$DATA_DIR/northeast.osrm.tld" ]; then
# v26 bakes the profile into the extract; customize takes no -p.
"$OSRM_BIN/osrm-customize" "$DATA_DIR/northeast"
fi fi
step "4/5 partition + customize" if [ "$SERVE" = 0 ]; then
"$OSRM_BIN/osrm-partition" --input-location "$DATA_DIR/northeast" echo "done (not serving; start with: $OSRM_BIN/osrm-routed --algorithm mld --port 5000 $DATA_DIR/northeast.osrm)"
"$OSRM_BIN/osrm-customize" --input-location "$DATA_DIR/northeast" --profile-file "$OSRM_BIN/../profiles/driving.lua" exit 0
fi
step "5/5 serve" step "6/6 serve on :5000 (Ctrl-C to stop)"
echo "Starting osrm-routed on :5000 (Ctrl-C to stop)"
exec "$OSRM_BIN/osrm-routed" --algorithm mld --port 5000 "$DATA_DIR/northeast.osrm" exec "$OSRM_BIN/osrm-routed" --algorithm mld --port 5000 "$DATA_DIR/northeast.osrm"