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

205 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 стр."}'
```