# 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 -> (опц. модуль «события по дому») -> фильтр по дому -> ответ ``` ## Быстрый старт ```bash 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 без сервиса: ```bash ./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). ## Тесты ```bash make test # go test -race ./... ``` Проверка парсера на реальном реестре (вне репозитория): ```bash 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: сеть не нужна. ```bash ./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`) Прогоняет **весь** реестр на самого себя (не выборку): ```bash ./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 ```bash 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 стр."}' ```