# OSRM routing engine (self-hosted) — #39

Self-hosted [OSRM](https://project-osrm.org/) для site-finder `/analyze`: реальные
дорожные / пешие расстояния до POI вместо straight-line `ST_Distance(geography)`.

> **Scope этого PR — INFRA ONLY.** Здесь стоит контейнер + build-скрипт + healthcheck.
> Интеграция в `/analyze` (замена `ST_Distance` на OSRM `route`/`table`) — отдельный
> follow-up (A2 изохроны, A3 per-category routing-decay). Backend пока к osrm НЕ ходит.

## Архитектура

```
backend ──http://osrm:5000──▶ osrm (osrm-routed --algorithm mld)
                                 │ volume ./data/osrm:/data
                                 └─ /data/sverdlovsk.osrm*  (MLD-граф, ~ХХХ MB)
```

- Compose service `osrm` в `docker-compose.prod.yml`, сеть `default` (project `gendesign`).
- **Наружу НЕ публикуется** — нет `ports:`. Достижим только из backend по `http://osrm:5000`.
  Caddy его не проксирует (routing — internal API, не должен быть публичным).
- `mem_limit: 1.5g`, `restart: unless-stopped`.
- Граф (`/data/*.osrm*`) НЕ в git (`.gitignore` → `data/osrm/*`). Строится offline на VPS.

## Build / refresh графа

Скрипт: [`scripts/build_osrm.sh`](../scripts/build_osrm.sh). Запускается **вручную**
на VPS (НЕ часть `deploy.yml` — download+build тяжёлые).

```bash
cd /opt/gendesign
bash scripts/build_osrm.sh            # car-профиль, Свердл-clip (default)
```

### Источник pbf и решение по региону

Geofabrik **не** публикует Свердловскую область отдельно — есть только
[Уральский ФО](https://download.geofabrik.de/russia/ural-fed-district-latest.osm.pbf)
(≈600 MB). Скрипт по умолчанию (`CLIP=1`) скачивает Ural-FO и **обрезает по bbox
Свердл. обл.** через `osmium extract` → легче граф, меньше RAM в runtime.

| Режим | Команда | pbf | Граф в RAM (warm) | Когда |
|---|---|---|---|---|
| **Свердл-clip** (default) | `bash scripts/build_osrm.sh` | clip из Ural-FO | ~0.8–1.2 GB | рекоменд., extensibility на обл |
| ЕКБ-tight | `EKB_TIGHT=1 bash scripts/build_osrm.sh` | узкий bbox ЕКБ | ~0.4–0.6 GB | если RAM на VPS критична |
| Ural-FO zero-clip | `CLIP=0 bash scripts/build_osrm.sh` | весь Ural-FO | ~1.5 GB+ | если нужен запас на соседние регионы |

bbox Свердл (с запасом): `lon 57.2..66.2, lat 56.0..62.0`. ЕКБ-tight: `lon 59.0..62.5, lat 56.2..58.2`.

### Pipeline (MLD, не CH)

`--algorithm mld` в compose требует pipeline **extract → partition → customize**
(скрипт делает это через тот же `osrm/osrm-backend` образ). `osrm-contract` —
это CH-алгоритм, он несовместим с `--algorithm mld`, поэтому НЕ используется.

### Walking-профиль + per-category routing (#39 A3 — LIVE)

Issue хочет оба профиля (car + foot). A3 их свёл в **per-category routing**: пешеходные
POI (школа, д/сад, магазины, аптека, парк, остановки) считаются по **пешему** графу
(`osrm-walk`), авто-POI (ТЦ, больница) — по **car** (`osrm`). Это убирает завышение
дистанций driving-профилем до близких-пешком POI (валидация #39: driving-медиана
1.6–2.9× straight-line). Маппинг категория→профиль: `settings.osrm_walk_categories`.

**Сборка пешего графа** (`sverdlovsk-foot.osrm*`):

```bash
CAR=0 WALK=1 bash scripts/build_osrm.sh   # только пеший граф, НЕ трогает живой car-граф
# (полный refresh обоих: WALK=1 bash scripts/build_osrm.sh)
```

`CAR=0` пропускает car-этап (foot-only refresh не перезаписывает работающий
`sverdlovsk.osrm`, который mmap'нут живым контейнером `osrm`).

**Compose-service `osrm-walk`** добавлен в `docker-compose.prod.yml` (рядом с `osrm`,
internal-only, `mem_limit 1.5g`). Backend ходит к нему по `http://osrm-walk:5000`
(`settings.osrm_walk_local_url`). До сборки графа контейнер crash-loop'ит — безвредно
(backend не depends_on, флаг OFF).

Поднять после сборки:

```bash
docker compose -p gendesign -f docker-compose.prod.yml up -d --no-deps osrm-walk
docker run --rm --network gendesign_default curlimages/curl:8 -fsS \
  'http://osrm-walk:5000/route/v1/foot/60.6,56.84;60.65,56.86'   # ожидаем code:Ok
```

### Monthly refresh

osm.pbf на Geofabrik обновляется ежедневно. Граф освежать ~ежемесячно:

```cron
0 4 1 * * cd /opt/gendesign && bash scripts/build_osrm.sh >> /var/log/osrm-build.log 2>&1 \
  && docker compose -p gendesign -f docker-compose.prod.yml up -d --force-recreate --no-deps osrm
```

Скрипт idempotent: `wget -N` качает только если на сервере новее, граф перестраивается overwrite.

## Развёртывание (что выполняет оркестратор на VPS)

```bash
cd /opt/gendesign
git fetch origin main && git reset --hard origin/main     # подтянуть compose + скрипт

# 1. Build графа (~30–50 мин, ~600MB download + ~3GB peak RAM на extract)
bash scripts/build_osrm.sh

# 2. Поднять контейнер
docker compose -p gendesign -f docker-compose.prod.yml up -d osrm

# 3. Verify (curl ВНУТРИ образа нет — используем sidecar curl в той же сети)
docker run --rm --network gendesign_default curlimages/curl:8 -fsS \
  'http://osrm:5000/route/v1/driving/60.6,56.84;60.65,56.86'
# ожидаем JSON: {"code":"Ok","routes":[{...}], ...}
```

## Healthcheck

Compose-healthcheck — TCP-probe порта 5000 через `bash /dev/tcp` (образ
`osrm/osrm-backend` НЕ содержит curl/wget, HTTP-probe падал бы на missing binary).
Полный route-probe — verify-команда выше (sidecar curl).

## Риски / open

- **RAM на Beget VPS**: +~1 GB warm для Свердл-clip. Перед `up -d osrm` проверить
  свободную RAM (`free -h`). Если жмёт — `EKB_TIGHT=1` (~0.4–0.6 GB) или upgrade plan.
- **Build на VPS медленный** (50+ мин). Альтернатива: собрать локально и `scp`
  `data/osrm/*.osrm*` в `/opt/gendesign/data/osrm/`.
- **Размер pbf неточен**: Ural-FO ≈600 MB на скачку; «1.5GB» из issue — оценка по
  всему ФО + intermediate-файлы на диске во время build (нужно ~5 GB свободного диска).
