Appearance
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 в GeoResolver | 0 |
| Обратная совместимость | 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, ResolvedAddress | UUID из addresses.address_id |
address_id в POST /vendor/products | UUID, ссылка на 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:
- Lookup
addresses.address_id→ получитьregion_id,city_id,district_id - Перевыставить эти поля в
products(нерегиональные поля игнорировать) - Lookup
address_coordinates→ найти ближайшее метро в радиусе 1500m (см. DEV-402 H-003)
Последствия
Положительные
- Смена провайдера без миграции фронта. Захотим завтра добавить DaData fallback для метро или OSM для радиусного поиска — добавляем запись в
addressesс другимsource,address_idостаётся стабильным. - Радиусный поиск становится возможным через
address_coordinatesбез изменения схемыproducts. - Аудит источника адреса.
addresses.sourceпоказывает, откуда пришёл адрес (важно при разборе fraud-карточек). - Изоляция фронта от ГАР-структуры. Если ГАР сменит формат OBJECTGUID или мы решим уйти на КЛАДР — фронт не трогаем.
- Закрывает premortem H-006 (GeoProvider абстракция становится реальной).
Отрицательные
- +1 уровень индирекции. Каждый suggest-результат теперь — это
INSERT ... ON CONFLICT DO UPDATE(или lookup + insert) вaddressesна бэке. SLO suggest должен учитывать этот overhead. - +1 таблица. Доп.место в БД (~30M записей
addresses≈ 10ГБ). - Migration cost. DEV-379 расширяется на schema-update + миграцию СПб/Москва.
Митигация overhead suggest
- При импорте ГАР предзаполнить
addressesдля всех адресов уровня ≥ city. Suggest поpg_trgmповерхaddresses.value. UUID уже сгенерирован, lookup не нужен. - Только динамическое создание
addressesпроисходит приPOST /store/geo/locate(геолокация по координатам — редкий путь).
Откатываемость
Если решение окажется неудачным:
- Бэк начинает принимать
address_idилиfias_id(обратно) вPOST /vendor/products - Фронт переключается на
fias_id-payload одним MR addresses.source_valueостаётся как метаданные
Двухфазный rollback возможен; не блокирует.
Альтернативы (отклонены)
- Опция B — fias_id напрямую: дешевле сейчас, но создаёт жёсткую связь с ГАР; смена провайдера потребует миграции БД и публичного контракта. Отклонено premortem H-006.
- Опция C — composite key (provider, source_id): избыточная сложность; UUID в одной колонке проще для FK и индексов.
- Опция D — не сохранять addresses, резолвить fias_id налету: ломает аудит и радиусный поиск; невозможно проверить, не изменился ли источник.
Открытые вопросы
- UUID генерация: на стороне БД (
gen_random_uuid()) или на стороне приложения? → БД (детерминированно, не зависит от языка backend). - Версионность
addresses: нужна ли история сменыsourceдля одного адреса? → Нет в MVP (UNIQUE по(source, source_value); если ГАР обновит source_value — это новая запись). 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