Files
addrkit/README.md
T
addrkit 66f4cef90d docs: position addrkit as a universal address service
- core = address normalization/resolution (universal, no EDDS coupling)
- "events by house" (outages) framed as an optional plugin module
- README: positioning, API table marks /v1/outages optional, selfcheck section,
  updated typo numbers and LLM variant-guard note
2026-10-09 20:57:20 +07:00

11 KiB
Raw Blame History

addrkit

Универсальный детерминированный сервис нормализации и поиска адресов. Разбирает вольные реплики (дроби, литеры, корпуса, строения, опечатки), сопоставляет их с каноническим реестром и отдаёт ответ только из данных — LLM не участвует в пути корректности.

Ядро продукта — только адрес: оно не привязано ни к ЕДДС, ни к отключениям. Опциональный модуль «события по дому» (отключения, аварии, плановые работы — данные, привязанные к конкретному дому) подключается отдельно; ЕДДС — лишь один из его потребителей.

Проект на Go, живёт офлайн (один статический бинарь + SQLite), без MCP в рантайме.

Позиционирование

  • Ядро (универсальное): нормализация, структурирование и поиск адреса по реестру, статусы found/partial/ambiguous/not_found, версия данных. Годится для колл-центра, доставки, CRM, аналитики, любого робота.
  • Опциональный модуль «события по дому»: получает канонический адрес из ядра и отдаёт события, привязанные к этому дому (совпадение дома, дроби, литеры и корпуса; строение игнорируется). Источник — plug-in (outages.Source): сегодня ArcGIS-портал ЕДДС, завтра любой фид (ES, SQL, API). Ядро про события ничего не знает.

Зачем

Существующая логика поиска адреса в диалоговом сценарии страдала от нечёткого поиска по всей строке (fuzziness: AUTO, size:1), из-за чего дробь/корпус/литера могли дать неверный дом. addrkit разделяет задачу на компоненты, сопоставляет дом строго и вместо угадывания возвращает явный статус (partial/ambiguous).

Архитектура

Гексагональная: домен чистый и без I/O, всё внешнее — в адаптерах.

cmd/addrkitd            сервис (HTTP)
cmd/addrkit             CLI: seed | verify | resolve | eval | selfcheck
internal/
  domain/address        нормализация и разбор адреса (чистые функции)
  domain/match          сопоставление с индексом, статусы   ← ядро
  domain/outages        «события по дому»: фильтр по дому   ← опциональный модуль
  adapters/registrystore SQLite (modernc, без CGO)
  adapters/arcgis       клиент источника событий (timeout/retry)  ← пример plug-in
  adapters/llm          опциональный rewriter (OpenAI-совместимый)
  adapters/httpapi      версионированный JSON API
  config                конфиг + ${VAR}
  seed                  сборка индекса из дампа реестра

Поток запроса:

"2-я Станционная улица 1/1 стр."
   -> address.Parse  -> {ordinal:2, name:станционная, house:1, frac:1}
   -> match.Resolve  -> exact street -> exact house -> FOUND
   -> (опц. модуль «события по дому») -> фильтр по дому -> ответ

Быстрый старт

make build

# 1. Собрать индекс из дампа реестра (Bulk NDJSON или JSONL).
./bin/addrkit seed -in registry.ndjson -out registry.db -version 2026-10-01

# 2. Запустить сервис.
cp config.example.json config.json
ADDRKIT_PORTAL_URL='https://map.example/.../101/query' ./bin/addrkitd -config config.json

# 3. Проверить.
curl -s localhost:6171/v1/resolve -d '{"address":"2-я Станционная улица 1/1 стр."}'
curl -s localhost:6171/v1/outages -d '{"address":"станционная 30а к9/1"}'

CLI без сервиса:

./bin/addrkit resolve   -db registry.db "кошурникова 11/1"
./bin/addrkit verify    -db registry.db
./bin/addrkit selfcheck -db registry.db

API v1

Метод Путь Назначение
POST /v1/resolve [ядро] адрес → {status, address, district, confidence, candidates[]}
POST /v1/outages [опц. модуль] адрес → канонический адрес + события/отключения по дому
GET /v1/healthz живость
GET /v1/readyz готовность (индекс загружен)
GET /v1/version версия данных

Статусы: found, partial, ambiguous, not_found, portal_timeout, infra_error.

Конфигурация

config.json (см. config.example.json). Значения поддерживают ${VAR}; незаданная переменная = ошибка запуска (fail closed). Секреты — только из окружения.

Промпт LLM-rewriter не зашит в код: встроенный дефолт лежит в internal/adapters/llm/default_prompt.txt и компилируется в бинарь (go:embed). Переопределить можно в конфиге: llm.system_prompt (инлайн) или llm.system_prompt_file (путь к файлу; приоритетнее инлайна).

Принятые решения

  • Детерминизм. Один вход — один выход, порядок кандидатов стабилен. resolve не зависит от сети.
  • Дом сравнивается строго. Литера/дробь/корпус должны совпасть; иначе partial, а не «похожий» дом.
  • LLM — опция, выключена по умолчанию. adapters/llm лишь предлагает варианты записи адреса; результат всегда берётся из реестра, поэтому модель не может «выдумать» адрес. Вариант принимается только если он исправляет написание улицы, не меняя слова, порядковый и дом (compatibleVariant).
  • Ядро универсально, события по дому — опциональный модуль. Ядро (domain/address, domain/match) не знает ни про ЕДДС, ни про отключения; источник событий — plug-in (outages.Source), поэтому модуль переиспользуем для любых данных, привязанных к дому.
  • Данные. Сервис владеет своим индексом, построенным из дампа реестра. Источник истины и регламент обновления — предмет отдельного согласования.
  • Часовой пояс. Даты отключений трактуются в Asia/Novosibirsk (UTC+7).

Тесты

make test          # go test -race ./...

Проверка парсера на реальном реестре (вне репозитория):

ADDRKIT_REGISTRY=/path/to/address_registry.ndjson \
  go test ./internal/seed -run TestRealRegistryParseRate -v

На дампе из 73 936 строк парсер распознаёт 99.98 % (14 строк — заведомо битые форматы; они не угадываются, а попадают в отчёт stats.unparsed_sample).

Оценка качества (addrkit eval)

Детерминированный harness генерирует «гражданские» искажения канонических адресов и измеряет, как резолвер справляется. Это offline: сеть не нужна.

./bin/addrkit eval -db registry.db -sample 800 -seed 1

Категории и результат на реальном реестре (по 800 адресам на seed):

Категория Пример искажения Точность
baseline канонический адрес как есть 100 %
case_lower / case_upper регистр 100 %
city_prefix г. Новосибирск, … 100 %
filler_dom … дом 5 100 %
punctuation лишние ,/пробелы 100 %
yo ё вместо е 100 %
type_abbrev ул./пер./пр-т 100 %
type_removed улица без слова «улица» 100 %
type_front улица Станционная 1 100 %
corpus_synonym корпус вместо к 100 %
structure_drop без стр. 100 %
typo опечатка в названии улицы ~78–82 % (остальное — ambiguous)
drop_ordinal / drop_letter / drop_fraction потеря 2-я / литеры / дроби потери (lossy)

Ключевой результат: 0 случаев, когда сервис подставил неверный дом («safety violations») на десятках тысяч сгенерированных кейсов. Опечатки либо распознаются, либо честно уходят в ambiguous/not_found, но не превращаются в чужой адрес.

С подключённым LLM-rewriter'ом (опция, llm.enabled) точность на опечатках поднимается до ~90 % при том же нуле неверных адресов (вариант модели принимается только если исправляет лишь написание улицы — см. «Принятые решения»).

Самопроверка (addrkit selfcheck)

Прогоняет весь реестр на самого себя (не выборку):

./bin/addrkit selfcheck -db registry.db

На реестре 72 825 записей: 72 811 (99.98 %) резолвятся точно, 14 — partial (битые форматы дома: 27с1, 32 к 1/4-1/6, 112с729), 0 not_found, 0 неверных.

Локальный dev

make build
./bin/addrkit seed -in registry.ndjson -out dev/registry.db -version 2026-10-01
cp config.example.json dev/config.json   # укажите data_path и portal.url
./bin/addrkitd -config dev/config.json
curl -s localhost:6171/v1/resolve -d '{"address":"2-я Станционная улица 1/1 стр."}'