- 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
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 стр."}'