Files
addrkit/README.md
T
Maksim Totmin f1537f1d6b addrkit: strict ordinal matching, LLM-variant guard, externalized prompt
- match: require candidate ordinal to equal the query ordinal in fuzzy search
  (deterministic typo recall 73% -> 80%, no wrong houses)
- resolver: compatibleVariant guard — LLM may only fix street spelling, keeping
  the same words, ordinal and house unit; damerau distance for transpositions
- llm: prompt moved out of code (go:embed default_prompt.txt) with
  llm.system_prompt / llm.system_prompt_file overrides; guided JSON parsing
  tolerant of thinking blocks
- address: recognize ordinals appearing after the street name
- tests: first coverage for the LLM adapter and config prompt loading; README

Measured (sample 200): deterministic 94.7-95.8% exact / 0 wrong across 4 seeds;
LLM+guard typo 90.7-89.8% / 0 wrong. LLM fallback latency ~1.2-1.9s.
2026-10-09 21:40:46 +07:00

166 lines
8.1 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 в
рантайме.
## Зачем
Существующая логика поиска адреса в диалоговом сценарии страдала от
нечёткого поиска по всей строке (`fuzziness: AUTO`, `size:1`), из-за чего
дробь/корпус/литера могли дать неверный дом. `addrkit` разделяет задачу на
компоненты, сопоставляет дом строго и вместо угадывания возвращает явный
статус (`partial`/`ambiguous`).
## Архитектура
Гексагональная: домен чистый и без I/O, всё внешнее — в адаптерах.
```
cmd/addrkitd сервис (HTTP)
cmd/addrkit CLI: seed | verify | resolve
internal/
domain/address нормализация и разбор адреса (чистые функции)
domain/match сопоставление с индексом, статусы
domain/outages выборка отключений по дому (чистая логика)
adapters/registrystore SQLite (modernc, без CGO)
adapters/arcgis клиент портала (timeout/retry)
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
-> (опц.) outages.Source -> фильтр по дому -> ответ
```
## Быстрый старт
```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
```
## 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` лишь предлагает
варианты записи адреса; результат всегда берётся из реестра, поэтому модель
не может «выдумать» адрес.
- **Данные.** Сервис владеет своим индексом, построенным из дампа реестра.
Источник истины и регламент обновления — предмет отдельного согласования.
- **Часовой пояс.** Даты отключений трактуются в `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** | опечатка в названии улицы | ~70–75 % (остальное — `ambiguous`) |
| drop_ordinal / drop_letter / drop_fraction | потеря `2-я` / литеры / дроби | потери (lossy) |
Ключевой результат: **0 случаев, когда сервис подставил неверный дом**
(«safety violations») на десятках тысяч сгенерированных кейсов. Опечатки либо
распознаются, либо честно уходят в `ambiguous`/`not_found`, но не превращаются
в чужой адрес.
## Локальный 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 стр."}'
```