Skip to content

ADR-014: Address Identifier Architecture

Дата: 2026-06-01 Статус: Accepted Контекст: DEV-402 (Геолокация и адресный флоу) — premortem H-006 Связанные документы:


Контекст

В рамках DEV-402 расширяем геолокацию Partizap на полную РФ. Backend импортирует ГАР (~30M записей до уровня дома), реализует свой suggest API и GeoProvider абстракцию для будущих провайдеров (DaData, КЛАДР, OSM).

Вопрос: какой идентификатор адреса фронт и публичный API используют как ключ?

Опции:

A — internal address_id (UUID)B — fias_id (UUID из ГАР)
ИсточникГенерируется бэком в таблице addressesИмпортируется из ГАР (OBJECTGUID)
Связь с источникамиaddresses (address_id, source, source_value)Жёсткая привязка к ГАР
Смена провайдера (КЛАДР / OSM / DaData)Добавить запись в addresses с другим sourceНевозможна без миграции БД и фронта
Радиусный поискОтдельная таблица coordinates (address_id, lat, lon, source) через FKНевозможен без отдельной таблицы координат, привязанной к fias_id
Цена+1 таблица + UUID generation в GeoResolver0
Обратная совместимостьLegacy продукты получают address_id = NULL (перевыбор по DEV-402 H-001)Аналогично

Решение

Принять опцию A — internal address_id (UUID).

Схема БД (backend)

sql
CREATE TABLE addresses (
  address_id  UUID PRIMARY KEY,
  source      TEXT NOT NULL CHECK (source IN ('gar', 'kladr', 'dadata', 'osm', 'manual')),
  source_value TEXT NOT NULL,
  level       TEXT NOT NULL CHECK (level IN ('region', 'city', 'district', 'street', 'house')),
  region_id   INTEGER REFERENCES regions(id),
  city_id     INTEGER REFERENCES cities(id),
  district_id INTEGER REFERENCES districts(id),
  street      TEXT,
  house       TEXT,
  value       TEXT NOT NULL,
  unrestricted TEXT,
  created_at  TIMESTAMP NOT NULL DEFAULT now(),
  UNIQUE (source, source_value)
);

CREATE TABLE address_coordinates (
  address_id UUID PRIMARY KEY REFERENCES addresses(address_id) ON DELETE CASCADE,
  lat        DOUBLE PRECISION NOT NULL,
  lon        DOUBLE PRECISION NOT NULL,
  source     TEXT NOT NULL CHECK (source IN ('gar', 'dadata', 'osm', 'manual')),
  precision  TEXT CHECK (precision IN ('region', 'city', 'street', 'house')),
  updated_at TIMESTAMP NOT NULL DEFAULT now()
);

source_value для source='gar' — это OBJECTGUID из ГАР (то, что раньше планировалось как fias_id). Для source='dadata' — это data.fias_id из DaData-ответа. И т.д.

Публичный контракт

ЧтоЗначение
address_id в AddressSuggestion, ResolvedAddressUUID из addresses.address_id
address_id в POST /vendor/productsUUID, ссылка на addresses
fias_id в публичном APIотсутствует (бэк-метаданные не утекают наружу)

Миграция текущих данных

  • Записи СПб/Москва получают строки в addresses через batch-импорт ГАР (см. DEV-379)
  • В products добавляется колонка address_id UUID NULL REFERENCES addresses(address_id)
  • Legacy продукты (СПб + Москва): address_id IS NULL до перевыбора через DEV-402 фронт-эпик

GeoResolver

POST /vendor/products с address_id:

  1. Lookup addresses.address_id → получить region_id, city_id, district_id
  2. Перевыставить эти поля в products (нерегиональные поля игнорировать)
  3. Lookup address_coordinates → найти ближайшее метро в радиусе 1500m (см. DEV-402 H-003)

Последствия

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

  1. Смена провайдера без миграции фронта. Захотим завтра добавить DaData fallback для метро или OSM для радиусного поиска — добавляем запись в addresses с другим source, address_id остаётся стабильным.
  2. Радиусный поиск становится возможным через address_coordinates без изменения схемы products.
  3. Аудит источника адреса. addresses.source показывает, откуда пришёл адрес (важно при разборе fraud-карточек).
  4. Изоляция фронта от ГАР-структуры. Если ГАР сменит формат OBJECTGUID или мы решим уйти на КЛАДР — фронт не трогаем.
  5. Закрывает premortem H-006 (GeoProvider абстракция становится реальной).

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

  1. +1 уровень индирекции. Каждый suggest-результат теперь — это INSERT ... ON CONFLICT DO UPDATE (или lookup + insert) в addresses на бэке. SLO suggest должен учитывать этот overhead.
  2. +1 таблица. Доп.место в БД (~30M записей addresses ≈ 10ГБ).
  3. Migration cost. DEV-379 расширяется на schema-update + миграцию СПб/Москва.

Митигация overhead suggest

  • При импорте ГАР предзаполнить addresses для всех адресов уровня ≥ city. Suggest по pg_trgm поверх addresses.value. UUID уже сгенерирован, lookup не нужен.
  • Только динамическое создание addresses происходит при POST /store/geo/locate (геолокация по координатам — редкий путь).

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

Если решение окажется неудачным:

  1. Бэк начинает принимать address_id или fias_id (обратно) в POST /vendor/products
  2. Фронт переключается на fias_id-payload одним MR
  3. addresses.source_value остаётся как метаданные

Двухфазный rollback возможен; не блокирует.

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

  • Опция B — fias_id напрямую: дешевле сейчас, но создаёт жёсткую связь с ГАР; смена провайдера потребует миграции БД и публичного контракта. Отклонено premortem H-006.
  • Опция C — composite key (provider, source_id): избыточная сложность; UUID в одной колонке проще для FK и индексов.
  • Опция D — не сохранять addresses, резолвить fias_id налету: ломает аудит и радиусный поиск; невозможно проверить, не изменился ли источник.

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

  1. UUID генерация: на стороне БД (gen_random_uuid()) или на стороне приложения? → БД (детерминированно, не зависит от языка backend).
  2. Версионность addresses: нужна ли история смены source для одного адреса? → Нет в MVP (UNIQUE по (source, source_value); если ГАР обновит source_value — это новая запись).
  3. address_coordinates — кто заполняет? → Backend-таск §7.2 DEV-402 (batch-геокодинг через OSM Nominatim или DaData; choice — отдельный sub-таск).

Action items

  • [ ] Backend: schema migration (см. §7.1 DEV-402)
  • [ ] Backend: GeoResolver реализация (см. §7.3 DEV-402)
  • [ ] Backend: ad-hoc rollback-plan документ если решение откатывается
  • [ ] Frontend: openapi.yaml обновить на address_id (см. фронт-имплементация Task 1-3)
  • [ ] Архитектор: ревью ADR

Принято

  • [x] Архитектор: Дмитрий Вязников
  • [x] Дата принятия: 2026-06-01
  • [ ] Backend lead: подпись отложена; согласование с бэк-командой не блокирует, синхронизация через DEV-425 + обновлённый DEV-379