Skip to content

ADR-015: Batch Geocoding Architecture

Дата: 2026-06-01 (sanity-check + OSM-match benchmark 2026-06-02; policy update 2026-06-03) Статус: Proposed — выбран Plan A+D hybrid (OSM auto-resolve + manual select fallback), ждёт утверждения архитектораКонтекст: DEV-402 (Геолокация и адресный флоу) — premortem H-003, DEV-426 §7.2 Связанные документы:

Policy update (2026-06-03): При работе над ADR-016 уточнена free/OSS политика — paid DaData запрещён в проекте (любой режим: runtime и batch). Free-tier DaData Suggestions API разрешён, но Geocoding API не имеет free tier → DaData batch fill-in (исходный Plan C+B) исключён. Текущая версия ADR — переработка под политику.


Контекст

ADR-014 зафиксировал internal address_id (UUID) как ключ адреса и таблицу address_coordinates. Для гибрида авто-метро в форме продавца (premortem H-003) /store/geo/resolve обязан вернуть coords: {lat, lon} и metro_station_id (если ближайшая станция < 1500m). ГАР XML-дамп ФИАС координат уровня здания не содержит — нужен отдельный batch-источник координат.

Скоуп MVP: 7 городов РФ с метро — Москва, Санкт-Петербург, Нижний Новгород, Казань, Екатеринбург, Новосибирск, Самара.

Бюджет: ≤ 50K ₽ разово.

Технологии: PHP Slim 4 + PostgreSQL (PostGIS возможен).

Реальный масштаб

OSM Overpass (sanity-check 2026-06-01): count(building+addr:housenumber) per город:

ГородOSM building+addr
Москва149,066
Санкт-Петербург85,842
Казань68,844
Новосибирск58,621
Самара37,354
Нижний Новгород35,607
Екатеринбург29,721
Итого OSM~465K

ГАР XML COUNT (DEV-433, sanity-check 2026-06-02): AS_HOUSES где ISACTIVE=1 AND ISACTUAL=1, привязанные к 7 целевым городам:

ГородGAR active+actualOSMΔ %
Москва289,680149,066+94%
Санкт-Петербург135,52585,842+58%
Казань160,89168,844+134%
Новосибирск126,38158,621+116%
Самара78,78837,354+111%
Нижний Новгород138,19135,607+288% ⚠️
Екатеринбург70,74329,721+138%
Итого ГАР1,000,199465,055+115%

Изначальная оценка 6-8M завышена в 6-8 раз. Фактический addressable set по ГАР ≈ 1.0M записей. Расхождение GAR vs OSM объясняется multiplicity записей (корпуса/строения/литеры в ГАР = 1 building в OSM, типично 2-3×). Аномалия по НН требует доп. проверки (LEVEL=2 после реформы марта 2026 может захватывать прилегающие посёлки).

Полная методология и расчёт бюджета — gar-count-2026-06-02.md.

Решение

Принять гибридный подход A+D — DIY OSM ETL pipeline даёт auto-resolve на ~12% адресов (OSM coverage), для остальных 88% продавец выбирает станцию метро вручную в UI (manual select fallback, premortem H-003). Без paid DaData fill-in (нарушает free/OSS политику).

Архитектура

┌─────────────────────┐
│ Geofabrik Russia    │
│ OSM PBF (~3.8GB)    │  ←──── ODbL 1.0, daily snapshot
└──────────┬──────────┘
           │ osmium / pyosmium

┌──────────────────────────────────┐
│ Extract nodes/ways с тегами:     │
│  building=*                      │
│  addr:city, addr:street,         │
│  addr:housenumber                │
│ → centroid lat/lon               │
└──────────┬───────────────────────┘
           │ libpostal нормализация
           │ streetmangler словарь
           │ (ул./улица, пр-кт/проспект, корпуса)

┌──────────────────────────────────┐
│ Fuzzy-match с ГАР записями       │
│ (addresses.source='gar')         │
│ Match score ≥ threshold          │
└──────────┬───────────────────────┘
           │ match (~12%) → INSERT address_coordinates(source='osm')

           │ no-match (~88%) → ничего; address_coordinates без записи

┌──────────────────────────────────────────────────────────┐
│ Runtime /store/geo/resolve(address_id):                  │
│  если address_coordinates row есть → coords + metro auto │
│  иначе → coords=null, UI показывает manual metro select  │
└──────────────────────────────────────────────────────────┘

Цикл регулярно перезапускается (ETL daily/weekly) — рост OSM coverage снижает долю manual-select адресов со временем. Pre-warm OSM contribution через mapping party (открытый вопрос #8) — long-term путь к ≥50% OSM coverage.

Стек

КомпонентТехнологияЛицензияОбоснование
Источник OSMGeofabrik Russia PBF (3.8GB)ODbL 1.0Бесплатно, daily snapshot, ODbL разрешает commercial use с attribution
Парсинг PBFosmium-tool / pyosmium / php-osmiumBSLIndustry-standard, эффективен на больших файлах
Нормализация адресовlibpostalMIT99.45% global parse accuracy (NB: RU отдельно не бенчмаркен — см. §Caveats)
Сокращения улицstreetmangler словарьMITПокрывает «ул./улица», «пр-кт/проспект», корпуса
Fallback для unmatched (~88%)Manual metro select в UI (DEV-402 фронт)Продавец выбирает станцию из dropdown; нет paid external API
Хранение координатaddress_coordinates (см. ADR-014) — отсутствие row = нет auto-resolveUUID PK на addresses.address_id; на pages без row UI fallback
Runtime запрос «метро в 1500m»PostGIS ST_DWithin + GIST + KNN <->Микросекунды на запись (только для адресов с coords)

Не используется

  • OSM Nominatim self-host — over-engineering для одноразового batch; создан под runtime queries, требует 64GB+ RAM для full planet (или 16-32GB для RU). Прямой парсинг osmium эффективнее.
  • DaData Geocode Batch API (любой режим — primary или fill-in) — нарушает free/OSS политику проекта (2026-06-03). Geocoding API не имеет free tier, 0.20 ₽/адрес × ~35-50K residual = 7-10K ₽ оплаты. Исключено единогласно с ADR-016 §«Не используется».
  • DaData как primary — 465K × 0.20 = 93K ₽; нелегитимно по free/OSS политике (см. выше).
  • Yandex Maps API Geocoder — $10K/год минимум (~900K ₽, 18× over budget) + историческое ToS-ограничение на хранение координат.
  • 2GIS Geocoder API — ToS §4.2 (law.2gis.ru/api-rules) прямо запрещает долговременное хранение результатов геокодинга.
  • OpenAddresses.io РФ — статический архив 2021, 15.4% покрытие, 7 целевых городов не enumerated.
  • Росреестр — нет documented batch coordinate export API.
  • Реформа ЖКХ (NextGIS dump) — только МКД, неполное покрытие ИЖС, формат не подходит под прямую интеграцию с ГАР.

Бюджет

Фактический OSM bottleneck: OSM-match benchmark (2026-06-02) показал — OSM coverage building+addr в России = ~12% от ГАР (120K из 1M записей). Никакое улучшение алгоритма (libpostal, deeper fuzzy) не превысит этого потолка на текущем состоянии OSM.

Результаты benchmark (sample 10,499 ГАР-адресов):

ГородOSM building+addrGARТеор. maxSample match (с false-pos)
Москва31,778289,68011.0%29.9%
Казань34,512160,89121.5%35.2%
Самара1,57678,7882.0%4.9%
Итого120,6321,000,19912.1%23.3%

Текущий 23.3% содержит false-positives (overly-permissive housenum нормализация). Реальный bound ≈ 12% OSM coverage.

Полный расчёт: osm-match-2026-06-02.md.

Варианты для решения (исторические)

ПланБюджетCoverageStatus
A — pure OSM 100% (исходный)~10K ₽❌ невозможен (OSM 12% bound)отброшен
B — DaData fill-in полное покрытие150-200K ₽100%отброшен по free/OSS политике
C — scope-cut «1500m буфер метро» c DaData50-100K ₽≈30-50% (в зоне метро)отброшен (содержит DaData)
D — manual metro select только~0 ₽0% auto, 100% manualrejected: UX downgrade для всех
E — OSM-rich cities first + DaData fill-in~50K ₽4 города full + 3 manualотброшен (содержит DaData)
A+D hybrid (принято)~10K ₽12% auto + 88% manual selectвыбрано 2026-06-03

Принято: A+D hybrid. OSM ETL даёт 12% адресов с auto-resolve. Для 88% продавец выбирает станцию метро вручную в UI (DEV-402 frontend). Без paid external API. Бюджет ~10K ₽ (только OSM ETL engineering).

Engineering effort (оценка)

4-8 человеко-дней backend + 2-3 дня frontend (manual select UI):

  1. osmium PBF extract + centroid calculation (1 день)
  2. libpostal нормализация ГАР streets (1-2 дня)
  3. Fuzzy-match streetmangler + Python (2-3 дня)
  4. QA + reconciliation reports (1-2 дня)
  5. Frontend manual metro select component (DEV-402) — dropdown с поиском, fallback UI когда address_coordinates row отсутствует (2-3 дня)

Последовательность работ

  1. Backend sanity-check ГАР COUNT (DEV-433): выполнен 2026-06-02 — 1.0M записей. Отчёт.
  2. OSM-match benchmark: выполнен 2026-06-02 — реальный bottleneck = OSM coverage ≈ 12%. Отчёт.
  3. Policy clarification (2026-06-03): free/OSS only, paid DaData запрещён → выбран Plan A+D hybrid.
  4. Schema: таблица address_coordinates (DDL в ADR-014)
  5. ETL Phase 1 — OSM extract: Geofabrik Russia PBF → osmium → building+addr nodes/ways → centroid (lat, lon) + raw address fields
  6. ETL Phase 2 — Match: libpostal нормализация → streetmangler словарь → fuzzy match с addresses по (region_id, city_id, street_normalized, house_number) → INSERT address_coordinates(source='osm')
  7. Validation: 10 случайных матчей МСК + 10 СПб проверить руками по карте → > 95% правильных
  8. Frontend manual metro select (DEV-402): dropdown компонент с поиском по metro_stations, появляется когда /store/geo/resolve вернул coords: null (адрес не в OSM coverage). Сохраняет выбор в форме продавца.
  9. Recurring ETL: weekly osm-pbf snapshot → re-run pipeline → новые OSM coverage апдейты постепенно снижают долю manual-select адресов.

Acceptance

GET /store/geo/resolve?address_id=<любой дом МСК или СПб> возвращает:

  • coords: {lat, lon} | null — null когда адрес не в OSM coverage (frontend показывает manual metro select)
  • metro_station_id: <id> | null — auto-resolved только если coords ≠ null И ближайшая станция ≤ 1500m
  • metro_distance_m: <int> | null — расстояние в метрах для auto-resolved случая

Покрытие: ≥ 12% адресов в addresses для 7 городов имеют запись в address_coordinates (auto-resolve). Остальные 88% — manual select в UI (premortem H-003 fallback теперь — основной путь для большинства адресов).

Последствия

Положительные

  1. Стоимость ~10K ₽ (только OSM ETL engineering effort, без paid external API)
  2. Open data — нет vendor lock-in; смена источника на любой другой провайдер не блокируется (ADR-014 addresses.source IN ('gar', 'kladr', 'dadata', 'osm', 'manual'))
  3. Контроль качества — на каждом этапе можно вставить custom rules (например, отбросить матчи с low score < 0.7)
  4. Расширяемость — добавить новый город = обновить геофильтр в ГАР, перезапустить pipeline
  5. Стабильность — runtime не зависит от внешнего API (только internal address_coordinates lookup)
  6. Соответствие free/OSS политике — нет paid DaData, согласовано с ADR-016
  7. Coverage growth со временем — recurring ETL подхватывает рост OSM contribution; manual-select fraction снижается естественно

Отрицательные

  1. 88% адресов требуют manual metro select — UX downgrade для большинства продавцов в форме. Premortem H-003 fallback теперь — основной путь, не редкий fallback
  2. Engineering effort 4-8 backend + 2-3 frontend дней против 1-2 дней при чистом DaData
  3. Качество libpostal RU не отдельно бенчмаркен — risk false negatives на сложных адресах
  4. OSM PBF обновляется ежедневно, ГАР — 2×/нед — нужна стратегия sync между источниками (см. §Открытые вопросы)
  5. ODbL share-alike — если публиковать derived address_coordinates external — под ODbL; для внутреннего use Partizap — без ограничений
  6. Зависимость от продавцов в выборе метро — корректность 88% metro_station_id зависит от disciplined input в форме (валидация дропдауном против списка станций митигирует)

Откатываемость

Если 12% OSM coverage оказался недостаточным для UX (например жалобы на manual select dominance):

  1. Pre-warm OSM contribution — mapping party с импортом ГАР building data в OSM (long-term, 1-2 года к ≥50% coverage). Открытый вопрос #8.
  2. Plan B revisited — переутверждение paid DaData fill-in бюджета с продактом/финансами (требует override free/OSS политики).
  3. OSM Nominatim self-host для адресов вне Geofabrik PBF — расширение алгоритма за счёт runtime queries (over-engineering для batch, но возможно если нужно).
  4. Alternative free data sources — re-evaluate OpenStreetMap planet (вместо Geofabrik RU), реформа ЖКХ NextGIS dump для МКД (частичное покрытие новостроек).

Multi-phase rollback возможен; не блокирует фронт-эпик DEV-432.

Альтернативы (отклонены)

ПодходСтоимостьПричина отклонения
OSM Nominatim self-host~100K setup + 10K/месOver-engineering под one-shot batch; Nominatim runtime-oriented
DaData Geocode (paid, любой режим)7-200K ₽Нарушает free/OSS политику (2026-06-03); Geocoding API не имеет free tier
Yandex Maps API~900K ₽/год18× over budget + ToS restrictions
2GIS APIToS §4.2 запрещает storage
OpenAddresses.io РФ0 ₽Архив 2021, 15.4% coverage, 7 целевых городов не enumerated
Росреестр PKK0 ₽Нет batch coordinate export API
Plan B (full DaData fill-in)80-150K ₽Нелегитимно по политике; ранее рекомендован, после policy update отброшен
Plan E (OSM-rich cities + DaData fill-in)~50K ₽Содержит paid DaData → отброшен

Открытые вопросы

  1. ГАР sanity-check (DEV-433, 2026-06-02): 1.0M записей.
  2. OSM-match benchmark (2026-06-02): реальный bottleneck = OSM coverage ≈ 12%, не алгоритм.
  3. Решение по плану (2026-06-03): A+D hybrid — Plan B/C/E содержали paid DaData → отброшены по free/OSS политике.
  4. Аномалия НН (+288% vs OSM): валидация descendants OBJECTID=889336 — действительно ли LEVEL=2 муниципальный округ захватывает прилегающие посёлки. Спец-фильтр может потребоваться.
  5. libpostal RU accuracy: не критично (bottleneck в OSM coverage, не алгоритме), но даст +2-3% точности для production ETL.
  6. OSM ↔ ГАР sync стратегия: OSM daily snapshot vs ГАР 2×/нед дельты — как часто перезапускать ETL? Sequence: ГАР delta → OSM PBF snapshot → diff → INSERT/UPDATE только новых матчей
  7. ODbL share-alike на derived database — если address_coordinates экспортируется в любую внешнюю систему (BI/analytics/affiliate-программу) — нужна legal review
  8. Pre-warm OSM contribution (long-term): mapping party + ГАР import → coverage до 50-80% за 1-2 года, уменьшит долю manual-select адресов
  9. Manual select UX (DEV-402): правильно ли продавцы выбирают станцию? Метрика accuracy через выборочный sample + cross-check с почтовым индексом или другой proxy. Если accuracy < 80% — пересмотреть UI или вернуться к плану B (paid DaData с переутверждением)
  10. Manual select discoverability: в каких случаях показывать поле метро вообще (только когда coords есть и в зоне 1500m от метро) vs всегда показывать как опциональное

Action items

Выполнено

  • [x] Backend (DEV-433): sanity-check ГАР COUNT — выполнен 2026-06-02. Отчёт.
  • [x] Backend (новый спайк): OSM-match benchmark — выполнен 2026-06-02. Отчёт.
  • [x] Policy clarification (2026-06-03): free/OSS only, paid DaData запрещён → выбран Plan A+D hybrid.

Blocking для prod-launch

  • [ ] Архитектор: review ADR-015 → Accepted (бюджет ~10K ₽ — engineering effort, не требует финансовых согласований).
  • [ ] Backend: валидация descendants для НН (OBJECTID=889336) — действительно ли соответствует именно городу.
  • [ ] Backend (опц.): libpostal benchmark на 1K → подтвердить +2-3% точности.
  • [ ] Backend (DEV-426 §7.2): ETL pipeline согласно §Последовательность работ — Phases 4-7 (OSM extract + match + validation).
  • [ ] Backend (DEV-426 §7.4): sample-валидация 30 случайных матчей по карте → ≥ 95% правильных (false-positive bound).
  • [ ] Frontend (DEV-402): manual metro select component — dropdown с поиском по metro_stations, появляется когда /store/geo/resolve вернул coords: null. Валидация выбора против списка станций.
  • [ ] Backend (DEV-426): endpoint /store/geo/resolve поведение coords: null для адресов без address_coordinates row + четкий контракт frontend manual select.

Recurring

  • [ ] Backend (DEV-426): weekly cron — OSM PBF snapshot → re-run ETL → расширение address_coordinates за счёт новых OSM contributions.

Долгосрочное

  • [ ] Community (long-term): mapping party + ГАР import в OSM для поднятия coverage > 50% за 1-2 года (открытый вопрос #8).
  • [ ] Legal: review ODbL share-alike implications если данные экспортируются external.
  • [ ] Backend: metric geo.resolve.manual_select_fraction — доля адресов без auto coords. При росте к 100% / устойчиво > 90% → пересмотр стратегии (Pre-warm OSM / переутверждение Plan B).

Принято

  • [ ] Backend lead: ___
  • [ ] Архитектор: ___
  • [x] План: A+D hybrid (выбран 2026-06-03 после free/OSS policy clarification)
  • [ ] Дата принятия: ___