addrkit: deterministic address resolution service (Go)
- domain: address normalization/parsing, matching with explicit statuses - adapters: SQLite registry store, ArcGIS outages client, optional LLM rewriter, HTTP API - seed pipeline with duplicate handling and unparsed reporting - CLI (seed/verify/resolve), systemd unit, OpenAPI contract, tests
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# 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). Секреты — только из
|
||||
окружения.
|
||||
|
||||
## Принятые решения
|
||||
|
||||
- **Детерминизм.** Один вход — один выход, порядок кандидатов стабилен.
|
||||
`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`).
|
||||
Reference in New Issue
Block a user