From d24bee5bd7b1628b494b48b99104a3be1fe18bfd Mon Sep 17 00:00:00 2001 From: frappuccino Date: Wed, 29 Jul 2026 11:33:35 +0300 Subject: [PATCH] Simplify repository for the 2.0.0a2 release --- .github/CODEOWNERS | 9 +- .github/FUNDING.yml | 11 - .github/ISSUE_TEMPLATE/bug-report.yml | 58 +- .github/ISSUE_TEMPLATE/config.yml | 7 +- .github/ISSUE_TEMPLATE/data-provenance.yml | 78 - .github/ISSUE_TEMPLATE/feature-request.yml | 48 +- .github/ISSUE_TEMPLATE/parsing-failure.yml | 76 - .github/ISSUE_TEMPLATE/security-contact.yml | 20 - .github/REPOSITORY_SETTINGS.md | 66 - .github/pull_request_template.md | 66 +- .github/workflows/ci.yml | 114 +- .github/workflows/release.yml | 103 +- .gitignore | 3 +- CHANGELOG.md | 45 +- CONTRIBUTING.md | 147 +- LICENSING.md | 44 - MANIFEST.in | 26 - PRODUCTION_PLAN.md | 348 --- README.en.md | 129 + README.md | 481 +-- README.ru.md | 396 --- SECURITY.md | 65 +- SUPPORT.md | 36 - api.py | 159 - app.py | 45 - benchmarks/README.md | 88 + .../legacy_500.jsonl | 0 .../legacy_500_results.csv | 0 docker-compose.yaml | 91 - docs/good-first-issues.md | 116 - docs/launch.md | 264 -- docs/ml-stack.md | 118 - docs/releasing.md | 98 - docs/triage.md | 106 - evaluation/DATA_SOURCES.md | 83 - evaluation/FAILURE_ANALYSIS.md | 102 - evaluation/README.md | 368 --- evaluation/RESULTS.md | 91 - evaluation/analyze_failures.py | 603 ---- evaluation/datamos_data.py | 105 - evaluation/datamos_manifest.json | 67 - evaluation/datamos_report.json | 776 ----- evaluation/deepparse_data.py | 199 -- evaluation/deepparse_manifest.json | 76 - evaluation/deepparse_report.json | 2226 ------------- evaluation/detection_reference.jsonl | 30 - evaluation/detection_report.json | 153 - evaluation/evaluate_datamos.py | 263 -- evaluation/evaluate_deepparse.py | 462 --- evaluation/evaluate_detection.py | 278 -- evaluation/evaluate_redmadrobot.py | 456 --- evaluation/evaluate_redmadrobot_detection.py | 318 -- .../legacy_reference_500_failure_summary.json | 170 - evaluation/legacy_reference_500_report.json | 958 ------ evaluation/prepare_datamos.py | 274 -- evaluation/prepare_deepparse.py | 487 --- evaluation/redmadrobot_detection_report.json | 2758 ----------------- evaluation/redmadrobot_report.json | 1528 --------- evaluation/release_gates.json | 19 - examples/README.md | 88 - examples/basic.py | 32 - examples/detect_in_message.py | 23 - examples/fastapi_app.py | 21 - examples/fias_gar_http.py | 88 - examples/jsonl_etl.py | 23 - parsing.py | 283 -- pyproject.toml | 2 +- ref/references.xlsx | Bin 600927 -> 0 bytes release-policy.toml | 16 - requirements-evaluation.txt | 4 - requirements-legacy.txt | 4 - scripts/build_reproducibly.py | 128 - scripts/check_artifacts.py | 434 --- scripts/check_license.py | 90 - scripts/check_release_ref.py | 64 - scripts/smoke_installed.py | 61 - scripts/test_artifact_policy.py | 67 - src/address_normalizer/data/model.json | 2 +- tests.py | 83 - {tests_v2 => tests}/test_adversarial.py | 0 {tests_v2 => tests}/test_api.py | 0 {tests_v2 => tests}/test_detection.py | 53 - {tests_v2 => tests}/test_model.py | 6 +- {tests_v2 => tests}/test_runtime_contract.py | 0 {tests_v2 => tests}/test_spans.py | 0 tests_v2/test_datamos_evaluation.py | 63 - tests_v2/test_deepparse_evaluation.py | 119 - tests_v2/test_evaluation.py | 77 - tests_v2/test_external_evaluation.py | 117 - tests_v2/test_failure_analysis.py | 85 - evaluation/evaluate.py => tools/benchmark.py | 43 +- .../real_corpus.py => tools/model_data.py | 0 .../baselines => tools}/synthetic_model.json | 0 .../train_model.py | 27 +- training/README.md | 71 - training/__init__.py | 1 - training/evaluate_compact_tagger.py | 95 - training/model_evaluation.json | 1316 -------- upload_fias.py | 197 -- 99 files changed, 470 insertions(+), 19624 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/data-provenance.yml delete mode 100644 .github/ISSUE_TEMPLATE/parsing-failure.yml delete mode 100644 .github/ISSUE_TEMPLATE/security-contact.yml delete mode 100644 .github/REPOSITORY_SETTINGS.md delete mode 100644 LICENSING.md delete mode 100644 MANIFEST.in delete mode 100644 PRODUCTION_PLAN.md create mode 100644 README.en.md delete mode 100644 README.ru.md delete mode 100644 SUPPORT.md delete mode 100644 api.py delete mode 100644 app.py create mode 100644 benchmarks/README.md rename evaluation/legacy_reference_500.jsonl => benchmarks/legacy_500.jsonl (100%) rename evaluation/legacy_reference_500_diagnostics.csv => benchmarks/legacy_500_results.csv (100%) delete mode 100644 docker-compose.yaml delete mode 100644 docs/good-first-issues.md delete mode 100644 docs/launch.md delete mode 100644 docs/ml-stack.md delete mode 100644 docs/releasing.md delete mode 100644 docs/triage.md delete mode 100644 evaluation/DATA_SOURCES.md delete mode 100644 evaluation/FAILURE_ANALYSIS.md delete mode 100644 evaluation/README.md delete mode 100644 evaluation/RESULTS.md delete mode 100644 evaluation/analyze_failures.py delete mode 100644 evaluation/datamos_data.py delete mode 100644 evaluation/datamos_manifest.json delete mode 100644 evaluation/datamos_report.json delete mode 100644 evaluation/deepparse_data.py delete mode 100644 evaluation/deepparse_manifest.json delete mode 100644 evaluation/deepparse_report.json delete mode 100644 evaluation/detection_reference.jsonl delete mode 100644 evaluation/detection_report.json delete mode 100644 evaluation/evaluate_datamos.py delete mode 100644 evaluation/evaluate_deepparse.py delete mode 100644 evaluation/evaluate_detection.py delete mode 100644 evaluation/evaluate_redmadrobot.py delete mode 100644 evaluation/evaluate_redmadrobot_detection.py delete mode 100644 evaluation/legacy_reference_500_failure_summary.json delete mode 100644 evaluation/legacy_reference_500_report.json delete mode 100644 evaluation/prepare_datamos.py delete mode 100644 evaluation/prepare_deepparse.py delete mode 100644 evaluation/redmadrobot_detection_report.json delete mode 100644 evaluation/redmadrobot_report.json delete mode 100644 evaluation/release_gates.json delete mode 100644 examples/README.md delete mode 100644 examples/basic.py delete mode 100644 examples/detect_in_message.py delete mode 100644 examples/fastapi_app.py delete mode 100644 examples/fias_gar_http.py delete mode 100644 examples/jsonl_etl.py delete mode 100644 parsing.py delete mode 100644 ref/references.xlsx delete mode 100644 release-policy.toml delete mode 100644 requirements-evaluation.txt delete mode 100644 requirements-legacy.txt delete mode 100644 scripts/build_reproducibly.py delete mode 100644 scripts/check_artifacts.py delete mode 100644 scripts/check_license.py delete mode 100644 scripts/check_release_ref.py delete mode 100644 scripts/smoke_installed.py delete mode 100644 scripts/test_artifact_policy.py delete mode 100644 tests.py rename {tests_v2 => tests}/test_adversarial.py (100%) rename {tests_v2 => tests}/test_api.py (100%) rename {tests_v2 => tests}/test_detection.py (61%) rename {tests_v2 => tests}/test_model.py (89%) rename {tests_v2 => tests}/test_runtime_contract.py (100%) rename {tests_v2 => tests}/test_spans.py (100%) delete mode 100644 tests_v2/test_datamos_evaluation.py delete mode 100644 tests_v2/test_deepparse_evaluation.py delete mode 100644 tests_v2/test_evaluation.py delete mode 100644 tests_v2/test_external_evaluation.py delete mode 100644 tests_v2/test_failure_analysis.py rename evaluation/evaluate.py => tools/benchmark.py (86%) rename training/real_corpus.py => tools/model_data.py (100%) rename {training/baselines => tools}/synthetic_model.json (100%) rename training/train_compact_tagger.py => tools/train_model.py (95%) delete mode 100644 training/README.md delete mode 100644 training/__init__.py delete mode 100644 training/evaluate_compact_tagger.py delete mode 100644 training/model_evaluation.json delete mode 100644 upload_fias.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 01e3d79..0eceb8f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,10 +1,7 @@ * @shigabeev -/.github/ @shigabeev -/LICENSING.md @shigabeev -/SECURITY.md @shigabeev -/MANIFEST.in @shigabeev -/evaluation/ @shigabeev +/.github/workflows/ @shigabeev +/benchmarks/ @shigabeev /src/address_normalizer/data/ @shigabeev -/training/ @shigabeev +/tools/ @shigabeev /pyproject.toml @shigabeev diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 4d75763..90aadda 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,13 +1,2 @@ -# These are supported funding model platforms - github: shigabeev -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username ko_fi: frappuccino_o -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 2c7a5f9..25d9c2e 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,69 +1,39 @@ name: Bug report -description: Report a reproducible API, CLI, installation, or packaging defect -title: "[Bug] " -labels: - - bug - - needs-triage +description: Report incorrect parsing, spans, detection, CLI, or packaging +title: "bug: " +labels: ["needs-triage"] body: - type: markdown attributes: - value: | - Thanks for a report maintainers can reproduce. For incorrect extracted - fields, use the Parsing failure form instead. Never include secrets, - private addresses, or unredacted production data. + value: Use a synthetic or redacted address. Never post private customer data. - type: input id: version attributes: - label: Package version or commit - description: Output of `python -c "import address_normalizer; print(address_normalizer.__version__)"` and, for a checkout, the commit SHA. - placeholder: 2.0.0a2 / abc1234 - validations: - required: true - - type: input - id: python - attributes: - label: Python and operating system - placeholder: Python 3.12.4 on Ubuntu 24.04 + label: Version + placeholder: 2.0.0a2 validations: required: true - type: textarea - id: install + id: input attributes: - label: Installation and exact command - description: Include the install source and the smallest command that triggers the defect. - render: shell + label: Minimal input + render: text validations: required: true - type: textarea id: actual attributes: - label: Actual behavior - description: Include the complete exception and traceback where applicable. + label: Actual result + render: json validations: required: true - type: textarea id: expected attributes: - label: Expected behavior - description: Explain the observable result you expected and why. + label: Expected result and why validations: required: true - type: textarea - id: reproduction - attributes: - label: Minimal reproduction - description: Prefer a self-contained snippet with synthetic data. - render: python - validations: - required: true - - type: checkboxes - id: checks + id: context attributes: - label: Submission checks - options: - - label: I reproduced this on the version or commit named above. - required: true - - label: I removed secrets and personal or production address data. - required: true - - label: This is not a security vulnerability requiring private disclosure. - required: true + label: Environment or additional context diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2466385..54f2b1f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,5 @@ blank_issues_enabled: false contact_links: - name: Security vulnerability - url: https://github.com/shigabeev/address-normalizer/security/policy - about: Read the private-reporting policy; do not disclose vulnerabilities publicly. - - name: Support and usage questions - url: https://github.com/shigabeev/address-normalizer/blob/master/SUPPORT.md - about: Check support scope and troubleshooting before opening an issue. + url: https://github.com/shigabeev/address-normalizer/security/advisories/new + about: Report vulnerabilities privately. diff --git a/.github/ISSUE_TEMPLATE/data-provenance.yml b/.github/ISSUE_TEMPLATE/data-provenance.yml deleted file mode 100644 index 9aadb8d..0000000 --- a/.github/ISSUE_TEMPLATE/data-provenance.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: Data provenance -description: Record source, permission, redistribution, or benchmark-integrity information -title: "[Provenance] " -labels: - - data - - provenance - - needs-triage -body: - - type: markdown - attributes: - value: | - Do not upload the dataset or sample rows unless redistribution is - clearly permitted. This form records evidence for maintainer review; it - does not itself authorize use. - - type: input - id: source - attributes: - label: Source and publisher - placeholder: Dataset title — publishing organization - validations: - required: true - - type: input - id: url - attributes: - label: Canonical source URL - placeholder: https://... - validations: - required: true - - type: textarea - id: version - attributes: - label: Version, revision, and retrieval date - description: Include immutable revision IDs and checksums where available. - validations: - required: true - - type: textarea - id: terms - attributes: - label: License or terms evidence - description: Link the exact terms and quote only the clause needed to explain use or redistribution. - validations: - required: true - - type: dropdown - id: intended_use - attributes: - label: Proposed role - options: - - Provenance correction only - - Independent sealed evaluation - - Train or validation data - - Redistributed derived artifact - - Runtime package data - - Other or undecided - validations: - required: true - - type: textarea - id: processing - attributes: - label: Filtering, transformations, and split policy - description: Include deduplication, grouping, leakage prevention, and personal-data handling. - validations: - required: true - - type: textarea - id: risks - attributes: - label: Open questions and risks - description: Note conflicting terms, stale snapshots, bias, private data, or attribution obligations. - validations: - required: true - - type: checkboxes - id: checks - attributes: - label: Submission checks - options: - - label: I have not attached unlicensed, private, or redistribution-restricted data. - required: true - - label: I am not treating this form as legal approval to use the source. - required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 5e745cf..5b091be 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -1,51 +1,23 @@ name: Feature request -description: Propose a user problem that fits the small offline parser boundary -title: "[Feature] " -labels: - - enhancement - - needs-triage +description: Propose a focused addition to the parser +title: "feature: " +labels: ["needs-triage"] body: - - type: markdown - attributes: - value: | - Start with the user problem. The core package will remain offline, - dependency-free at runtime, and separate from FIAS/GAR resolution. - type: textarea id: problem attributes: - label: User problem - description: Who needs this, in what workflow, and what fails today? - validations: - required: true - - type: textarea - id: outcome - attributes: - label: Smallest useful outcome - description: Describe observable behavior without prescribing an implementation. + label: Problem + description: Describe the user need, not an implementation. validations: required: true - type: textarea - id: alternatives + id: example attributes: - label: Alternatives considered - description: Could application code, a resolver, or an integration example solve this outside the core package? + label: Minimal example and desired result validations: required: true - type: textarea - id: compatibility - attributes: - label: API, data, and maintenance implications - description: Note serialized-shape changes, runtime cost, dependencies, data provenance, and who will maintain it. - validations: - required: true - - type: checkboxes - id: boundary + id: tradeoffs attributes: - label: Product boundary - options: - - label: The request does not require bundled FIAS/GAR data or address verification in the parser. - required: true - - label: The request does not require a hidden download, mandatory service, or new scientific runtime. - required: true - - label: I searched existing issues and documentation for this need. - required: true + label: Scope and trade-offs + description: Explain why this belongs in the offline parser rather than a FIAS/GAR resolver. diff --git a/.github/ISSUE_TEMPLATE/parsing-failure.yml b/.github/ISSUE_TEMPLATE/parsing-failure.yml deleted file mode 100644 index 0f0b3ec..0000000 --- a/.github/ISSUE_TEMPLATE/parsing-failure.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Parsing failure -description: Share one redacted or synthetic address with expected fields and spans -title: "[Parse] " -labels: - - parsing - - needs-triage -body: - - type: markdown - attributes: - value: | - One minimal case is more useful than a private corpus dump. Replace real - personal data with a synthetic address that still reproduces the - behavior. Confidence is not a correctness probability. - - type: input - id: version - attributes: - label: Package version or commit - placeholder: 2.0.0a2 / abc1234 - validations: - required: true - - type: textarea - id: input - attributes: - label: Minimal address input - description: Use synthetic or safely redacted text; preserve punctuation and Unicode relevant to the failure. - render: text - validations: - required: true - - type: textarea - id: actual - attributes: - label: Current `parse(...).as_dict()` output - render: json - validations: - required: true - - type: textarea - id: expected - attributes: - label: Expected fields and offsets - description: List each expected value and `[start, end)` span. Explain ambiguous interpretations. - placeholder: | - street: value="...", span=[0, 10] - house_num: value="...", span=[12, 13] - validations: - required: true - - type: dropdown - id: domain - attributes: - label: Input domain - description: This helps compare evidence without mixing benchmark domains. - options: - - Synthetic minimal reproduction - - User-entered or noisy address - - Clean registry-style address - - Historical v1 compatibility case - - Other or unknown - validations: - required: true - - type: textarea - id: impact - attributes: - label: Application impact - description: Which downstream decision is wrong, and can resolver/review policy catch it? - validations: - required: true - - type: checkboxes - id: checks - attributes: - label: Submission checks - options: - - label: The input is synthetic or safely redacted and contains no private address data. - required: true - - label: I checked warnings, alternatives, and unparsed output rather than only normalized text. - required: true - - label: I understand that this report may become a public regression example. - required: true diff --git a/.github/ISSUE_TEMPLATE/security-contact.yml b/.github/ISSUE_TEMPLATE/security-contact.yml deleted file mode 100644 index 563cf53..0000000 --- a/.github/ISSUE_TEMPLATE/security-contact.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Private security contact request -description: Request a private channel only when GitHub private vulnerability reporting is unavailable -title: "[Security contact] Private channel requested" -labels: - - security - - needs-triage -body: - - type: markdown - attributes: - value: | - Do not describe the vulnerability here. This public issue only asks a - maintainer to arrange a private reporting channel. Use GitHub's private - vulnerability report instead whenever it is available. - - type: checkboxes - id: disclosure - attributes: - label: Public disclosure check - options: - - label: I have included no vulnerability details, exploit, secret, private address, or other sensitive information in this issue. - required: true diff --git a/.github/REPOSITORY_SETTINGS.md b/.github/REPOSITORY_SETTINGS.md deleted file mode 100644 index 4af7775..0000000 --- a/.github/REPOSITORY_SETTINGS.md +++ /dev/null @@ -1,66 +0,0 @@ -# Suggested repository settings - -These are maintainer-facing settings for the GPL-3.0-only v2 package. - -## About section - -**Description** - -> Small offline Python parser for unstructured Russian addresses, with typed -> fields, source offsets, warnings, and no bundled FIAS/GAR database. - -**Website** - -Leave empty until a maintained documentation or package page exists. - -**Topics** - -```text -address-parsing -python -russian -nlp -offline -fias -gar -data-quality -``` - -`fias` and `gar` describe the downstream integration boundary, not bundled -registry data or identifier lookup. Do not add `geocoding`, `address-validation`, -`production-ready`, or an accuracy claim. - -## Community settings - -- Enable Issues and the issue-form chooser. -- Enable private vulnerability reporting before announcing a release. -- Keep blank issues disabled while the structured forms are new. -- Use Discussions only if a maintainer is prepared to moderate and answer them. -- Do not enable automatic deletion of branches or merge methods without first - checking the release and backport process. - -## Branch and review settings - -For the release branch, require: - -- pull requests and at least one maintainer review; -- approval from code owners for package data, training, workflows, and release - metadata; -- passing required checks; -- resolution of review conversations; -- no force pushes or branch deletion. - -Keep pull-request workflows unprivileged. Do not use `pull_request_target` to -check out or execute contributor code. - -## Labels - -Create the labels in [`docs/triage.md`](../docs/triage.md) manually or with an -explicitly reviewed maintainer script. No GitHub API calls have been made for -these suggestions. - -## Release state - -The license scope and data/model provenance decisions are recorded in -`LICENSING.md`. Publishing still requires the tagged build, artifact, and -protected-environment checks in `docs/releasing.md`. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 125ed48..3ec61f7 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,62 +1,12 @@ -> Contributions are accepted under GPL-3.0-only. Confirm that every submitted -> code, data, model, and generated artifact may be contributed under that -> license. See `LICENSING.md`. +## What problem does this solve? -## Linked issue or reproduced bug + -Closes # +## What changed? - +## Verification -## Behavior and maintenance impact - - - -## Evidence - - - -- Test that fails before and passes after: -- Focused test result: -- Full `pytest` result: -- Other checks: - -## Benchmark and artifact impact - - - -- Before/after metrics by relevant domain and field: -- Known regressions: -- Model-size delta: -- Wheel-size delta: -- Data source, version, license/terms, and checksum: - -## Author verification - -- [ ] This change is linked to an issue or includes a complete reproduced bug. -- [ ] I added a test that fails before the change and passes afterward, or - explained why no test applies. -- [ ] I ran the focused tests and the complete local suite and reported exact - results above. -- [ ] Original source offsets, ambiguity, warnings, alternatives, and unparsed - evidence are preserved where relevant. -- [ ] I did not add a runtime dependency, network call, hidden download, - FIAS/GAR data, or service requirement. -- [ ] I checked relevant benchmark domains rather than optimizing only one - aggregate score. -- [ ] I understand every submitted change, including automated or AI-assisted - portions, and can explain and maintain it. -- [ ] I have the right to submit every code, data, model, and documentation - artifact in this pull request. -- [ ] No secrets, private addresses, production logs, caches, or large external - corpora are included. +- [ ] Added or updated a focused test +- [ ] `pytest` passes +- [ ] Relevant benchmark still passes +- [ ] No private address data, runtime dependency, or hidden network behavior diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bce3176..7543cae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,17 +2,12 @@ name: CI on: push: - branches: [master, main] + branches: [master] pull_request: - workflow_dispatch: permissions: contents: read -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - jobs: test: name: Python ${{ matrix.python-version }} @@ -22,106 +17,47 @@ jobs: matrix: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - name: Check out source - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: ${{ matrix.python-version }} - cache: pip - cache-dependency-path: pyproject.toml - - name: Install test tools and package - run: | - python -m pip install "pytest==8.4.2" - python -m pip install --no-deps . - - name: Run tests - run: python -m pytest + - run: python -m pip install pytest==8.4.2 + - run: python -m pip install --no-deps . + - run: python -m pytest static-analysis: runs-on: ubuntu-latest steps: - - name: Check out source - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.12" - cache: pip - cache-dependency-path: pyproject.toml - - name: Install mypy - run: python -m pip install "mypy==1.17.1" - - name: Check package typing - run: python -m mypy + - run: python -m pip install mypy==1.17.1 + - run: python -m mypy model-and-regression: runs-on: ubuntu-latest steps: - - name: Check out source - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.12" - - name: Install package - run: python -m pip install --no-deps . - - name: Verify compact model - run: | - python training/evaluate_compact_tagger.py - python training/train_compact_tagger.py \ - --output "${RUNNER_TEMP}/regenerated-model.json" \ - --report "${RUNNER_TEMP}/regenerated-model-report.json" - cmp src/address_normalizer/data/model.json "${RUNNER_TEMP}/regenerated-model.json" - - name: Enforce legacy-reference release gates - run: | - python evaluation/evaluate.py \ - --data evaluation/legacy_reference_500.jsonl \ - --gates evaluation/release_gates.json + - run: python -m pip install --no-deps . + - run: python tools/benchmark.py --check package: runs-on: ubuntu-latest - env: - SOURCE_DATE_EPOCH: "1704067200" steps: - - name: Check out source - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.12" - cache: pip - cache-dependency-path: pyproject.toml - - name: Install build and metadata tools - run: python -m pip install "build==1.3.0" "twine==6.2.0" - - name: Verify publication license and provenance - run: python scripts/check_license.py --require-publishable - - name: Build byte-identical wheel and source distribution twice - run: python scripts/build_reproducibly.py --output dist - - name: Inspect contents, dependencies, checksums, and size - run: | - python scripts/check_artifacts.py dist \ - --write-manifest artifact-manifest.json - python scripts/test_artifact_policy.py dist - python -m twine check dist/*.whl dist/*.tar.gz - - name: Install wheel without an index - run: | - python -m venv "${RUNNER_TEMP}/wheel-venv" - "${RUNNER_TEMP}/wheel-venv/bin/python" -m pip install \ - --no-index --no-deps dist/*.whl - cp scripts/smoke_installed.py "${RUNNER_TEMP}/smoke_installed.py" - cd "${RUNNER_TEMP}" - "${RUNNER_TEMP}/wheel-venv/bin/python" -I smoke_installed.py - "${RUNNER_TEMP}/wheel-venv/bin/address-normalizer" \ - "Москва, Тверская 1" > single.json - echo "Ополченская 5-30" | - "${RUNNER_TEMP}/wheel-venv/bin/address-normalizer" --jsonl > batch.jsonl - "${RUNNER_TEMP}/wheel-venv/bin/python" -I -c \ - "import json; json.load(open('single.json')); [json.loads(line) for line in open('batch.jsonl')]" - - name: Upload inspected distributions - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: distributions - path: | - dist/ - artifact-manifest.json - if-no-files-found: error - retention-days: 14 + - run: python -m pip install build==1.3.0 twine==6.2.0 + - run: python -m build + - run: python -m twine check dist/* + - run: | + python -m venv /tmp/address-normalizer-smoke + /tmp/address-normalizer-smoke/bin/pip install --no-index --no-deps dist/*.whl + cd /tmp + /tmp/address-normalizer-smoke/bin/python -c \ + "from address_normalizer import parse; assert parse('Тверская 1').house_num.value == '1'" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 390488a..bd0c83f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,10 @@ -name: Build and publish release +name: Release on: workflow_dispatch: inputs: version: - description: "Expected PEP 440 package version (for example, 2.0.0a2)" + description: "Package version, for example 2.0.0a2" required: true type: string repository: @@ -12,73 +12,40 @@ on: required: true default: testpypi type: choice - options: - - testpypi - - pypi + options: [testpypi, pypi] run-name: Release ${{ inputs.version }} to ${{ inputs.repository }} permissions: contents: read -concurrency: - group: release-${{ github.ref }} - cancel-in-progress: false - jobs: build: runs-on: ubuntu-latest - env: - SOURCE_DATE_EPOCH: "1704067200" steps: - - name: Check out selected release commit - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - with: - fetch-depth: 0 - - name: Require the immutable matching release tag - env: - EXPECTED_VERSION: ${{ inputs.version }} - run: python scripts/check_release_ref.py --version "${EXPECTED_VERSION}" - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.12" - cache: pip - cache-dependency-path: pyproject.toml - - name: Install release tools - run: | - python -m pip install \ - "build==1.3.0" \ - "mypy==1.17.1" \ - "pytest==8.4.2" \ - "twine==6.2.0" - - name: Verify tests and typing - run: | - python -m pip install --no-deps . - python -m pytest - python -m mypy - - name: Verify license and model provenance - run: python scripts/check_license.py --require-publishable - - name: Build and inspect candidate + - run: python -m pip install build==1.3.0 pytest==8.4.2 twine==6.2.0 + - run: python -m pip install --no-deps . + - name: Verify tag and version env: EXPECTED_VERSION: ${{ inputs.version }} run: | - python scripts/build_reproducibly.py \ - --output release-artifact/packages - python scripts/check_artifacts.py release-artifact/packages \ - --expected-version "${EXPECTED_VERSION}" \ - --write-manifest release-artifact/artifact-manifest.json - python scripts/test_artifact_policy.py release-artifact/packages - python -m twine check \ - release-artifact/packages/*.whl \ - release-artifact/packages/*.tar.gz - - name: Upload candidate for maintainer review - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + PACKAGE_VERSION="$(python -c 'import address_normalizer; print(address_normalizer.__version__)')" + test "${GITHUB_REF_TYPE}" = "tag" + test "${GITHUB_REF_NAME}" = "v${EXPECTED_VERSION}" + test "${PACKAGE_VERSION}" = "${EXPECTED_VERSION}" + - run: python -m pytest + - run: python tools/benchmark.py --check + - run: python -m build + - run: python -m twine check dist/* + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - name: release-candidate-${{ inputs.version }} - path: release-artifact/ + name: distributions-${{ inputs.version }} + path: dist/ if-no-files-found: error - retention-days: 30 publish: needs: build @@ -90,40 +57,20 @@ jobs: contents: write id-token: write steps: - - name: Check out the exact candidate source - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: - python-version: "3.12" - - name: Download the inspected candidate - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: - name: release-candidate-${{ inputs.version }} - path: release-artifact/ - - name: Recheck license, version, contents, and checksums - env: - EXPECTED_VERSION: ${{ inputs.version }} - run: | - python scripts/check_license.py --require-publishable - python scripts/check_artifacts.py release-artifact/packages \ - --expected-version "${EXPECTED_VERSION}" \ - --verify-manifest release-artifact/artifact-manifest.json - - name: Publish with PyPI Trusted Publishing - uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # release/v1 + name: distributions-${{ inputs.version }} + path: dist/ + - uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # release/v1 with: - packages-dir: release-artifact/packages/ + packages-dir: dist/ repository-url: ${{ inputs.repository == 'testpypi' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }} - skip-existing: false - verbose: true - name: Create GitHub release if: ${{ inputs.repository == 'pypi' }} env: GH_TOKEN: ${{ github.token }} run: | - gh release create "${GITHUB_REF_NAME}" \ - release-artifact/packages/* \ - release-artifact/artifact-manifest.json \ + gh release create "${GITHUB_REF_NAME}" dist/* \ --verify-tag \ --title "address-normalizer ${GITHUB_REF_NAME}" \ --generate-notes diff --git a/.gitignore b/.gitignore index 9ca1970..cb43515 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,8 @@ __pycache__/ *.py[cod] *.egg-info/ .pytest_cache/ +.mypy_cache/ .cache/ .venv/ build/ dist/ -/artifact-manifest.json -/release-artifact/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f34a25f..20a7cfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,40 +1,13 @@ # Changelog -All notable user-visible changes will be recorded here. This project follows -[Semantic Versioning](https://semver.org/) and uses -[PEP 440](https://peps.python.org/pep-0440/) version syntax. +## 2.0.0a2 — 2026-07-29 -## Unreleased +- Added a dependency-free typed parser for Russian address strings. +- Added conservative address-span detection for free-form messages. +- Preserved source offsets, unparsed content, warnings, and alternatives. +- Added JSON/JSONL CLI support and a compact bundled sequence model. +- Published separate benchmark domains and a detailed 500-row failure table. +- Added Russian and English documentation. +- Licensed the project under GPL-3.0-only. -No user-visible changes yet. - -## 2.0.0a2 - 2026-07-29 - -### Added - -- Typed, dependency-free v2 parsing API and JSON/JSONL command-line interface. -- Lazy `parse_iter()` batches, predictable batch input errors, and public - typed-dictionary serialization schemas. -- Conservative `detect_addresses()` message-span detection with typed, - offset-preserving results and a positive/negative behavior fixture. -- Compact bundled sequence model for residual unmarked text. -- Independent evaluation reports with explicit metric-family names and release - regression gates; legacy report keys remain compatibility aliases. -- A committed results index, full historical benchmark report, and explicit - documentation of the hybrid rules/structured-perceptron runtime stack. -- A complete 500-row diagnostic CSV with per-field outcomes, scenario columns, - triage hypotheses, and a representative failure summary. -- Distribution inspection, isolated-wheel smoke tests, size limits, and - artifact checksum manifests. -- GNU GPL v3 licensing, recorded model provenance, a Russian README, and - protected TestPyPI/PyPI Trusted Publishing. - -### Changed - -- Source distributions now exclude evaluation corpora, training inputs, tests, - notebooks, caches, and historical v1 assets. -- CI covers every declared Python minor version from 3.10 through 3.14. -- Message-span detection is explicitly presented as a conservative alpha - feature rather than a complete arbitrary-prose recognizer. - -`2.0.0a1` was an internal development version and was not published. +`2.0.0a1` was an unpublished development version. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d7da58d..04338e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,144 +1,27 @@ # Contributing -Thank you for helping make Russian address parsing easier to inspect and trust. -Useful contributions here are usually small: one clearly reproduced behavior, -one bounded change, and evidence a maintainer can rerun. +Keep changes small and explain the user-visible problem they solve. -## License - -The project is licensed under GNU GPL v3 (`GPL-3.0-only`). By submitting a -contribution, you confirm that you have the right to provide it under that -license. Code, examples, generated models, and data-derived artifacts need -clear provenance; see [`LICENSING.md`](LICENSING.md). - -## Choose the right report - -- [Bug report](https://github.com/shigabeev/address-normalizer/issues/new?template=bug-report.yml): - installation, API, CLI, packaging, or deterministic runtime failures. -- [Parsing failure](https://github.com/shigabeev/address-normalizer/issues/new?template=parsing-failure.yml): - one address whose extracted fields, spans, warning, or alternative are - unexpected. -- [Feature request](https://github.com/shigabeev/address-normalizer/issues/new?template=feature-request.yml): - a user problem, not a preselected implementation. -- [Data provenance](https://github.com/shigabeev/address-normalizer/issues/new?template=data-provenance.yml): - source, permission, redistribution, or benchmark-integrity information. -- Security-sensitive reports follow [`SECURITY.md`](SECURITY.md), never a public - issue with exploit or private-address details. - -Remove or replace personal data before posting. A synthetic address that -reproduces the behavior is preferable. - -## Before a pull request - -1. link an existing issue or provide a complete, locally reproducible bug; -2. agree on scope before public API, model, data, dependency, or workflow work; -3. keep the change narrowly focused and preserve unrelated behavior; -4. add a test that fails before the fix and passes afterward; -5. explain the behavior in your own words, including maintenance implications; -6. report before/after output and relevant benchmark domains; -7. run the checks below and include exact results in the pull request. - -A benchmark delta alone is not a product improvement. Parser or model changes -must not trade away another domain, field, original offsets, ambiguity, or -unparsed evidence to improve an aggregate score. - -## Local setup and checks - -Runtime development needs no third-party package dependency: +## Setup ```bash python -m pip install -e . +python -m pip install pytest mypy pytest +python -m mypy ``` -For a parsing change, show the focused failing test first, then run the complete -suite. For example: - -```bash -pytest tests_v2/test_api.py -q -pytest -``` - -For documentation examples: - -```bash -python examples/basic.py -printf '%s\n' 'Ополченская 5-30' | python examples/jsonl_etl.py -python -m compileall -q examples -``` - -Model, evaluation, build, and release work has additional checks and provenance -requirements. Read -[`training/README.md`](https://github.com/shigabeev/address-normalizer/blob/master/training/README.md) -and -[`evaluation/README.md`](https://github.com/shigabeev/address-normalizer/blob/master/evaluation/README.md) -before starting it. Large -external data and its preparation dependencies must stay outside the runtime -package. - -## Parsing-change evidence - -Include: - -- the smallest synthetic or redacted input that reproduces the problem; -- expected fields and `[start, end)` offsets; -- output before and after the change; -- a regression test; -- an explanation of warnings, alternatives, and unparsed content affected; -- results for every relevant committed regression gate. - -Do not copy examples from a sealed test set into tests or tune against that set -while continuing to call it untouched. Never commit private addresses or -unreviewed production logs. - -## Model or benchmark changes - -Discuss these in an issue before implementation. A proposal must identify: - -- source, publisher, version/revision, URL, and retrieval date; -- license or terms and whether redistribution is allowed; -- transformations and filters; -- split and deduplication policy, including leakage prevention; -- exact reproduction command and checksums; -- before/after per-domain and per-field metrics; -- model and wheel size deltas; -- known regressions and rejected alternatives. - -Keep historical, noisy-window, nationwide clean-address, and official-registry -scores separate. Do not select only the friendliest metric or average -incompatible domains. - -## Changes requiring maintainer agreement - -Ask before changing: - -- public API or serialized result shape; -- supported Python versions; -- confidence semantics or review policy; -- runtime dependencies; -- model, training data, or data preparation; -- FIAS/GAR integration inside the core package; -- CI, permissions, release, or publishing workflows; -- large generated or binary artifacts. - -The core package must remain small, dependency-free at runtime, offline, and -independent of any bundled FIAS/GAR database or service. - -## Human accountability and automated assistance - -AI-assisted contributions can be reviewed. The human author must be able to: - -- explain every behavior change and why the approach is maintainable; -- identify the test that proves the bug and fix; -- reproduce claimed benchmark results; -- answer review questions and support follow-up repairs; -- confirm that submitted code and data may be contributed. +Parser changes should include: -Unexplained generated changes, benchmark-only optimizations, bulk formatting, -and changes whose author cannot maintain them will be closed. +1. a minimal synthetic or redistributable failing input; +2. expected component values and source spans; +3. a regression test that fails before the fix; +4. the full test result and, when relevant, `python tools/benchmark.py --check`. -## Review and triage +Do not hide ambiguity by discarding warnings, alternatives, or unparsed text. +Do not add runtime dependencies, network calls, or a registry database without +first discussing the product boundary in an issue. -Maintainers use [`docs/triage.md`](docs/triage.md) for labels, duplicate handling, -security routing, benchmark evidence, and review boundaries. Bounded starter -proposals are in [`docs/good-first-issues.md`](docs/good-first-issues.md). +Never publish private customer addresses in an issue or test. Contributors must +have the right to submit all code, data, and generated artifacts. Contributions +are licensed under GPL-3.0-only. diff --git a/LICENSING.md b/LICENSING.md deleted file mode 100644 index 85e1fc8..0000000 --- a/LICENSING.md +++ /dev/null @@ -1,44 +0,0 @@ -# Licensing and model provenance - -## Maintainer decision - -Effective 2026-07-29, the repository maintainer selected the GNU General -Public License version 3 for this project. The repository is distributed under -the `GPL-3.0-only` SPDX expression. [`LICENSE`](LICENSE) is the unmodified -`gpl-3.0` template returned by GitHub's license API so GitHub and package tools -can identify it consistently. - -The maintainer also authorizes redistribution of the historical -`ref/references.xlsx` workbook, the deterministic -`evaluation/legacy_reference_500.jsonl` derivative, and the compact model -derived from those rows as repository and package artifacts under -`GPL-3.0-only`. - -## Provenance record - -- The workbook first appears in repository commit - `4a72605b0204e2ba4c21f09d74c249b066c41021`, authored by Ilya Shigabeev - (`beat@live.ru`). -- Repository history for the workbook and v2 model contains only the - maintainer identities `Ilya Shigabeev` and `frappuccino`, using the same - `beat@live.ru` email address. -- The source-verifiable 500-row derivative is committed as - `evaluation/legacy_reference_500.jsonl`. -- `training/train_compact_tagger.py` deterministically regenerates the bundled - `src/address_normalizer/data/model.json`. -- `training/model_evaluation.json` records the source dataset digest, split, - training algorithm, tuning, test results, and artifact size. -- CI regenerates the model and requires a byte-for-byte match with the bundled - artifact. - -This record covers artifacts distributed by this repository and package. Large -external Deepparse, RedMadRobot, and Moscow source corpora are not included in -the wheel or source distribution; their separate source and license records are -documented in `evaluation/DATA_SOURCES.md`. - -## Contributions - -Contributions are accepted under the repository's `GPL-3.0-only` license. -Contributors must have the right to submit their code, data, and generated -artifacts and must record data/model provenance as described in -[`CONTRIBUTING.md`](CONTRIBUTING.md). diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index abcee35..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,26 +0,0 @@ -include LICENSE -include README.md -include README.ru.md -include CHANGELOG.md -include CONTRIBUTING.md -include LICENSING.md -include SECURITY.md -include SUPPORT.md -include pyproject.toml -recursive-include src/address_normalizer *.py -include src/address_normalizer/py.typed -include src/address_normalizer/data/model.json -recursive-include docs *.md -recursive-include examples *.md *.py - -exclude requirements-legacy.txt -prune .cache -prune evaluation -prune ref -prune tests_v2 -prune training -global-exclude *.py[cod] -global-exclude __pycache__ -global-exclude .DS_Store -global-exclude *.ipynb -global-exclude *.xlsx diff --git a/PRODUCTION_PLAN.md b/PRODUCTION_PLAN.md deleted file mode 100644 index 5cb4716..0000000 --- a/PRODUCTION_PLAN.md +++ /dev/null @@ -1,348 +0,0 @@ -# Production plan for address-normalizer v2 - -Status: P0/P1 hardening implemented for `2.0.0a2` on -`codex/v2-parser-sprint`; GPL-3.0-only licensing and model provenance are -recorded, and the alpha release is in progress. - -This document records the current evidence and the gates for a releasable v2. -It does not declare the package production-ready. Publication still requires a -tagged, reproducible build and protected TestPyPI/PyPI release checks. - -## Current-state audit - -### Product boundary - -Version 2 is a small, deterministic, offline Russian address **parser**. It -extracts an unverified interpretation with original character offsets. It is -not a FIAS/GAR resolver, geocoder, spelling authority, address validator, -service, or database. - -- Version: `2.0.0a2`. -- Supported Python declared in package metadata: 3.10 through 3.14. -- Runtime dependencies: none. -- Public entry points: `parse()`, `parse_many()`, lazy `parse_iter()`, - conservative `detect_addresses()`, `ParsedAddress`, `DetectedAddress`, - `AddressPart`, `Alternative`, and the matching serialized `TypedDict` - schemas. -- CLI: one-address JSON and stdin JSONL modes. -- Runtime model: 37,130-byte JSON linear-chain tagger. -- `2.0.0a2` candidate wheel: 45,843 bytes. -- Large corpora are ignored under `.cache/external/`; none is package data. -- Historical v1 root files are retained but are outside the `src/` package. - -### API and behavior - -The parser combines offset-preserving tokenization, explicit marker and numeric -rules, a compact sequence tagger for residual text, and deterministic -post-processing. Results retain raw substrings, `[start, end)` offsets, -unparsed spans, warnings, alternatives, and bounded confidence values. -Confidence is decision strength, not a calibrated probability. - -Closed API audit items: - -- `parse()`, `parse_many()`, and `parse_iter()` now have a consistent, - explicitly tested input-error contract. -- Empty input, Unicode whitespace, `ё/е`, long and malformed input, one-shot - iterables, and concurrent calls have explicit tests and documentation. -- `as_dict()` has a documented JSON-compatible schema with public `TypedDict` - types. -- Batch parsing remains eagerly list-based. The additive `parse_iter()` helper - provides one-pass lazy iteration and documents consumption and error timing. -- Supported source values, warning codes, alternative reasons, and the review - policy are part of the public alpha contract. - -### Reliability evidence - -The four evidence domains remain separate because their sources, schemas, and -metrics answer different questions: - -| Domain | Size | Primary metric | `2.0.0a2` baseline | -| --- | ---: | --- | ---: | -| Historical bank-shaped reference | 500 rows | exact component micro F1 | 95.9% | -| RedMadRobot noisy address windows | 578 windows | same-label span-overlap F1 | 58.7% | -| Deepparse nationwide clean strings | 100,000 rows | character-overlap F1 | 66.2% | -| Moscow official clean buildings | 15,196 rows | exact component-value F1 | 85.4% | - -Additional named baselines: - -- Historical exact-address match: 80.4%; no residual word/number tokens: 76.8%. -- RedMadRobot street-and-house slice: 78.8% span-overlap F1. -- Deepparse binary span-overlap F1: 84.4%; exact token-boundary sequence: - 8.0%. -- Moscow exact full-address match: 64.9%; street F1: 66.2%; house F1: - 98.2%; корпус F1: 99.6%; строение F1: 97.0%. -- Compact tagger grouped test: 96.5% token accuracy, 94.3% complete-sequence - accuracy, and 96.9% micro entity F1. The split has no meaningful district or - settlement coverage. - -The historical 500-row gate is small enough for normal CI. RedMadRobot and the -large corpora stay opt-in and must not be downloaded by routine test or build -jobs. No implementation work in this sprint may be tuned against those sealed -external test results. - -### Packaging, release, and repository - -- `setuptools` builds from `src/`; model JSON and `py.typed` are declared as - package data. -- Project metadata names the repository, Python range, console script, and has - no runtime dependencies. -- CI now tests every declared Python minor from 3.10 through 3.14, checks - deterministic model regeneration, runs the legacy gate, and builds - distributions. -- Release gates cover strict typing, metadata validation, exact wheel - allowlisting/forbidden-content inspection, clean wheel installation, CLI - smoke tests, explicit wheel/model budgets, sdist review, and a Trusted - Publishing workflow with named GitHub environments. -- Contributor, security, CODEOWNERS, PR, issue, support, triage, starter-work, - release, examples, and launch-draft assets are present. -- The worktree initially contained unrelated untracked notebook artifacts: - `Untitled.ipynb` and `.ipynb_checkpoints/`. They are not part of this sprint - and must remain untouched and uncommitted. - -## Users and primary use cases - -1. **Application developer** — install a tiny wheel, parse one user-supplied - address, inspect ambiguity, then query the application's own FIAS/GAR - resolver. -2. **Data/ETL engineer** — stream JSONL or an iterable of strings through an - offline, dependency-free parser while retaining raw values and offsets for - audit and correction. -3. **API developer** — expose the typed result from FastAPI or another service - without hidden network, filesystem, or process requirements. -4. **Evaluator/data steward** — reproduce named gates, understand the exact - scoring boundary and provenance, and keep independent domains separate. -5. **Contributor/maintainer** — reproduce a bug, add a fail-before/pass-after - test, measure cross-domain and artifact impact, and release only through a - reviewable workflow. - -Non-users include anyone needing existence verification, FIAS/GAR identifiers, -geocoding, authoritative correction, fuzzy registry search, or an embedded -current registry. Those needs require a downstream resolver. - -## Public API contract - -For the v2 pre-release series: - -- `parse(text: str) -> ParsedAddress` parses exactly one string and raises - `TypeError` for non-strings. -- `parse_many(addresses: Iterable[str]) -> list[ParsedAddress]` consumes the - iterable once, preserves order, returns an eager list, and uses the same - per-item validation as `parse()`. -- `parse_iter(addresses: Iterable[str]) -> Iterator[ParsedAddress]` preserves - order, consumes once, avoids preloading, and raises element errors when - iteration reaches them. -- `detect_addresses(text: str) -> tuple[DetectedAddress, ...]` returns ordered, - non-overlapping half-open spans in a free-form message. Detection is - conservative and requires a street marker plus a building, or an explicit - address cue plus a parseable street and building. -- Empty and whitespace-only strings return a valid empty `ParsedAddress`; they - do not invent components. -- `AddressPart.raw == ParsedAddress.raw[start:end]`; offsets are half-open - indices into the original Python string. -- `as_dict()` returns a JSON-compatible, stable v2 schema. Component keys remain - present with `null` when absent; tuple fields serialize as arrays. -- `normalized` is a convenient rendering of the parser's unverified - interpretation, not a canonical registry address. -- Confidence is a bounded ranking/review signal. No threshold may be described - as a probability or guarantee. -- Warnings, alternatives, and unparsed content are public information, not - debug output. Additive warning codes or alternative reasons may appear in - pre-releases; removing result fields or changing their meaning requires - migration notes. -- Parsing performs no network calls, downloads, service startup, or filesystem - writes. A process-local immutable model may be cached and shared across - threads. - -## Release decisions - -The maintainer selected GPL-3.0-only and authorized redistribution of the -historical workbook, its 500-row derivative, and the compact model. The complete -decision and reproducible provenance chain are recorded in `LICENSING.md`. -External Moscow, Deepparse, and RedMadRobot source corpora remain outside the -distribution under their separately recorded terms. - -### Engineering blockers for a stable `2.0.0` - -- Close all P0 gates below on every supported Python version. -- Independently review a documented sample of the legacy evaluation rows or - create a replacement gold benchmark with a sealed final split. -- Improve or explicitly accept the weak administrative and exact-street - behavior; do not hide it behind aggregate metrics. -- Freeze and document the v2 serialization, warning, and compatibility policy. -- Perform a TestPyPI rehearsal through the protected release environment. - -## Prioritized work - -### P0 — required before the next published pre-release - -- Make API input/serialization/empty-input behavior typed, tested, and - documented. -- Add adversarial regression tests for punctuation, casing, `ё/е`, Unicode - whitespace, compound houses, корпус/строение, apartments, missing markers, - reordered components, ambiguous numeric tails, one-shot iterables, and - concurrency. -- Add a strict CI packaging job that builds sdist/wheel, validates metadata and - contents, enforces wheel/model size budgets, installs the wheel into a clean - environment, and smoke-tests import plus both CLI modes without network. -- Add an explicit type-checker configuration and gate the public package. -- Add a GitHub Actions release workflow using OIDC Trusted Publishing, - immutable artifacts, protected environments, and an explicit version-tag - check. -- Add a changelog and release checklist that put licensing/provenance before - publication. -- Rewrite the README around the two-minute path, honest boundaries, named - benchmarks, result review, FIAS/GAR handoff, API/CLI reference, migration, - troubleshooting, and “Should I use this?” guidance. -- Add executable examples and issue/PR/security/support/triage assets. - -Acceptance criteria: - -- Unit tests pass on locally available interpreters and CI covers every - declared Python minor. -- Static type checking passes with the selected, committed configuration. -- The historical 500-row gate meets every value in - `evaluation/release_gates.json`; no named external baseline regresses as a - side effect of code changes. -- Two consecutive deterministic model builds are byte-identical. -- Built metadata validates; a clean environment installs only the built wheel, - imports from outside the checkout, parses an address, and runs single/JSONL - CLI smoke tests. -- Wheel runtime content is allowlisted and excludes `.cache`, corpora, - notebooks, training/evaluation data, legacy code, secrets, and forbidden - dependencies. -- Model is at most 65,536 bytes; wheel is at most 262,144 bytes. Actual - values are recorded in release evidence. -- README examples execute, README/package metadata render checks pass, and every - numeric reliability claim names its domain and metric. - -### P1 — high value for the beta - -- Exercise and document the lazy batch iterator in streaming integrations. -- Add review-policy recipes for strict/manual/lenient queues based on warnings, - alternatives, unparsed spans, and decision-strength confidence. -- Add reproducible FastAPI, ETL, JSONL, and customer-managed FIAS/GAR resolver - examples without adding runtime dependencies. -- Make benchmark report schemas and metric names self-explanatory; add cheap - fixture-level evaluator tests and provenance validation to routine CI. -- Publish a bounded set of useful starter-issue proposals and a release demo - script/recording plan. -- Document alternative-comparison methodology without unverified competitor - claims, plus maintainer-approved draft launch copy. - -Acceptance criteria: - -- Lazy parsing consumes a generator once and does not materialize it. -- Example source files are syntax-checked; dependency-bearing examples clearly - separate optional dependencies from the core package. -- Fixture-level evaluation runs offline in normal CI, while large downloads - require explicit commands. -- Contributor templates require a linked/reproduced problem, fail-before and - pass-after evidence, cross-domain consideration, and human understanding. - -### P2 — after beta evidence - -- Curate training/validation data for street and administrative fields, with - explicit rights and a new untouched final test. -- Add confidence calibration only if a representative labeled validation set - supports it; otherwise retain decision-strength semantics. -- Report confidence intervals and regional/source-system slices. -- Rehearse TestPyPI, verify attestations and installation, then promote an - unchanged artifact to PyPI with maintainer approval. -- Consider separately versioned integration adapters; keep the core runtime - registry-independent. - -## Decisions and unresolved maintainer questions - -Recorded engineering decisions: - -- Keep `parse_many()` eager and list-returning for compatibility. -- Prefer an additive lazy helper over changing `parse_many()` semantics. -- Keep runtime dependency-free and all data preparation dependencies separate. -- Keep the four benchmark domains and their metric families separate. -- Use PEP 440 progression `2.0.0aN` → `2.0.0bN` → `2.0.0rcN` → `2.0.0`; - do not skip directly from this alpha to stable. -- Build once per release tag and publish that reviewed artifact through Trusted - Publishing; never rebuild between test and publish. - -Maintainer decisions still required: - -1. Should historical v1 remain in the default branch for v2 stable, move to a - named archival directory/branch, or be removed only in a future major - cleanup? -2. Which GitHub environment will protect TestPyPI/PyPI publication, and who may - approve it? -3. What administrative/street quality threshold is acceptable for beta, and - who will perform the independent row review? -4. Should the eventual stable support policy include every Python minor - 3.10–3.14, or follow a rolling set once 3.10 reaches end of upstream support? - -## Proposed release sequence - -1. Complete and review P0. -2. Release `2.0.0a2` to TestPyPI from an approved tag; verify hashes, - attestations, wheel contents, offline install, CLI, and rollback procedure. -3. Release `2.0.0a2` to PyPI only with explicit maintainer approval. -4. Expand human-reviewed validation coverage and close chosen quality targets; - publish `2.0.0b1`. -5. Freeze API/serialization and documentation, run the sealed final evaluation - once, and publish `2.0.0rc1`. -6. Promote the reviewed release-candidate code and evidence to `2.0.0`; rebuild - only for a new version if any input changes. - -## Evidence record for this sprint - -Recorded on 2026-07-28 and rerun for the 2026-07-29 release candidate: - -- Unit suite: 74 tests passed; fresh installed-wheel smoke tests passed on - Python 3.10 and 3.14. CI covers 3.10 through 3.14. -- Strict `mypy==1.17.1`: success on all nine public package modules. -- Compact tagger: deterministic regeneration was byte-identical; the model is - 37,130 bytes with SHA-256 - `c23c4f3cf308b1f36f56d0679df278d66b3aeae41c70a2144d88606199f3eb36`. -- Historical 500-row gate: 80.4% exact-address match, 76.8% no-unparsed rate, - and 95.8538% exact component micro F1; every committed gate passed. -- External baselines remained exactly unchanged: RedMadRobot span-overlap F1 - 58.7462% (street-and-house slice 78.8392%); Deepparse span-overlap F1 - 84.3948%, character-overlap F1 66.2289%, and token-label F1 66.4946%; - Moscow exact component-value micro F1 85.3620% and exact-address match - 64.9316%. -- Two consecutive final builds were byte-identical. The wheel is 45,843 bytes - (SHA-256 - `e98a49d0d7e8514485230bdccab043e6dedf0991c177a1c183fd561f734b1650`); - the sdist is 73,601 bytes (SHA-256 - `e439ab16e0af50424d36c519c6fc309bafb3167af35ad3fb3b21f4f5b892bb1c`). -- The model remained 37,130 bytes, the wheel stayed below its 256 KiB budget, - and the larger source archive includes both English and Russian - documentation. -- Wheel metadata, archive safety, exact runtime allowlist, hashes, artifact - policy rejection tests, Twine rendering, no-index/no-dependency installation, - import, one-address CLI, and JSONL CLI checks all passed. -- GPL-3.0-only licensing and model provenance were subsequently approved on - 2026-07-29; `release-policy.toml` now enforces the publishable state. - -Committed benchmark baselines must not be rewritten merely to make a change -look successful. Timing-only fields may vary when the reports are reproduced. - -Detection and diagnostics addendum, recorded on 2026-07-29: - -- `detect_addresses()` adds conservative, offset-preserving message-span - detection without changing parser output on the historical regression. -- The detection behavior fixture contains 30 narrow messages: 18 positive and - 12 negative, with 20 exact address spans. All currently pass, but this - authored fixture is not an independent accuracy benchmark. -- The historical diagnostic table contains all 500 rows and more than 75 - columns covering per-field outcomes, scenario dimensions, warnings, - unparsed evidence, and heuristic triage causes. It records 402 exact and 98 - non-exact rows. -- Unit tests pass on locally available Python 3.10, 3.13, and 3.14: 74 passed - per interpreter. Strict `mypy==1.17.1` passes all nine package modules. -- The historical parsing gate remains unchanged at 80.4% exact-address match, - 76.8% no-unparsed rate, and 95.8538% exact component micro F1. -- On complete RedMadRobot messages, the development-only detector diagnostic - reports 98.0% any-overlap precision, 68.1% recall, 80.3% F1, and 100% - negative-message specificity. Its failures were inspected, so it is not a - sealed final-test result. -- The reproducible wheel is now 33,104 bytes with SHA-256 - `15f38aa4dde86da620d05d4e8e620624769b26430c7b365b9f62d2575c87c8ed`; - the sdist is 52,762 bytes with SHA-256 - `e45303316773c560473e16e12fe2f574967d4593cf3d0b7c778d7fa9a3d7dda3`. diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..3acd1cb --- /dev/null +++ b/README.en.md @@ -0,0 +1,129 @@ +# address-normalizer + +A small offline parser for Russian addresses. It extracts typed fields while +preserving source substrings and offsets, supports Python 3.10+, and has no +runtime dependencies. + +[Русская версия](https://github.com/shigabeev/address-normalizer#readme) + +## Install + +```bash +python -m pip install --pre address-normalizer +``` + +## Use + +```python +from address_normalizer import parse + +result = parse("г. Москва, ул. Тверская, д. 4, кв. 12") + +print(result.city.value) # Москва +print(result.street.value) # Тверская +print(result.house_num.value) # 4 +print(result.apartment.value) # 12 +print(result.normalized) # Москва, ул Тверская, д 4, кв 12 +``` + +Every component includes its normalized `value`, exact source substring `raw`, +half-open `start:end` offsets, decision source, and `confidence`. +`result.as_dict()` returns a JSON-compatible dictionary. + +Ambiguity remains visible: + +```python +result = parse("Ополченская 5-30") + +print(result.normalized) # Ополченская, д 5, кв 30 +print(result.warnings) # ("ambiguous_numeric_tail",) +print(result.alternatives) # includes compound house 5-30 +``` + +`confidence` is internal decision strength, not the probability that an address +exists. Review results containing `warnings`, `alternatives`, or `unparsed`. + +### Detect an address in a message + +```python +from address_normalizer import detect_addresses + +message = "Доставить по адресу: Москва, ул. Тверская, д. 13. Позвоните." + +for item in detect_addresses(message): + print(item.text) # Москва, ул. Тверская, д. 13 + print(item.span) # offsets in the original message + print(item.parsed) # ParsedAddress +``` + +Detection is conservative: weak candidates are intentionally rejected to avoid +treating dates or order numbers as addresses. + +### Batches and CLI + +```python +from address_normalizer import parse_many + +results = parse_many(["Тверская 1", "Невский проспект 10"]) +``` + +```bash +address-normalizer "СПб, Невский проспект 10, корп. 2" +printf '%s\n' "Тверская 1" "Ополченская 5-30" | + address-normalizer --jsonl +``` + +## Scope + +The package extracts postal and administrative fields, street and street type, +building/unit fields, source offsets, warnings, alternatives, and unparsed +content. + +It does not validate addresses against FIAS/GAR, return registry IDs, correct +official spelling, or geocode. Pass extracted candidates to a current registry +resolver owned by your application. + +## Implementation + +The runtime is a straightforward hybrid: offset-preserving tokenization, +explicit address and numeric rules, a compact linear sequence tagger for +unmarked words, and deterministic post-processing. It is not an LLM or neural +network. The model is 37 KB; there are no network calls or hidden downloads. + +## Quality + +One headline “accuracy” would mix incompatible tasks, so benchmarks stay +separate: + +| Dataset | Size | Metric | Result | +| --- | ---: | --- | ---: | +| Historical address sample | 500 | exact component micro F1 | 95.9% | +| Noisy address snippets | 578 | span-overlap F1 | 58.7% | +| Clean nationwide addresses | 100,000 | character-overlap F1 | 66.2% | +| Moscow buildings | 15,196 | exact component micro F1 | 85.4% | + +See +[`benchmarks/README.md`](https://github.com/shigabeev/address-normalizer/blob/master/benchmarks/README.md) +for definitions, per-field results, and +the complete 500-row diagnostic table. + +Building numbers, корпус, and строение are strongest. Administrative levels, +exact street boundaries, rare abbreviations, and unmarked numeric tails are +weaker. + +## Development + +```bash +python -m pip install -e . +python -m pip install pytest +pytest +python tools/benchmark.py --check +``` + +Runtime code lives in `src/address_normalizer`, core tests in `tests`, and the +reproducible failure set in `benchmarks`. + +## License + +GNU GPL v3.0 only. See +[`LICENSE`](https://github.com/shigabeev/address-normalizer/blob/master/LICENSE). diff --git a/README.md b/README.md index ca2c79a..82e513d 100644 --- a/README.md +++ b/README.md @@ -1,467 +1,132 @@ # address-normalizer -**Turn an unstructured Russian address into typed, offset-preserving fields—locally, with no runtime dependencies or registry download.** +Небольшой офлайн-парсер российских адресов для Python 3.10+. Извлекает поля, +сохраняет исходные подстроки и смещения, не требует runtime-зависимостей. -[Русская версия](https://github.com/shigabeev/address-normalizer/blob/master/README.ru.md) +[English README](https://github.com/shigabeev/address-normalizer/blob/master/README.en.md) -`address-normalizer` v2 is a small parser for applications that already have, -or plan to choose, their own FIAS/GAR lookup. It extracts components; it does -not verify an address, return a FIAS ID, geocode, or silently download data. - -## 30-second quick start +## Установка ```bash -python -m pip install address-normalizer -address-normalizer "Ополченская 5-30" +python -m pip install --pre address-normalizer ``` -Or use the typed Python API: +## Использование ```python from address_normalizer import parse -result = parse("Ополченская 5-30") +result = parse("г. Москва, ул. Тверская, д. 4, кв. 12") -print(result.normalized) -# Ополченская, д 5, кв 30 +print(result.city.value) # Москва +print(result.street.value) # Тверская +print(result.house_num.value) # 4 +print(result.apartment.value) # 12 +print(result.normalized) # Москва, ул Тверская, д 4, кв 12 ``` -That compact input is ambiguous. The parser keeps the source offsets, its -chosen interpretation, a warning, and a plausible compound-house alternative: - -```json -{ - "normalized": "Ополченская, д 5, кв 30", - "street": { - "value": "Ополченская", - "raw": "Ополченская", - "span": [0, 11], - "confidence": 0.78, - "source": "model" - }, - "house_num": {"value": "5", "span": [12, 13]}, - "apartment": {"value": "30", "span": [14, 16]}, - "warnings": ["ambiguous_numeric_tail"], - "alternatives": [ - { - "components": {"house_num": "5-30"}, - "reason": "A hyphenated numeric tail can also be a compound house number." - } - ] -} -``` - -The complete stable dictionary shape is available through `result.as_dict()`; -the shortened JSON above highlights the decision. Confidence is a bounded -**decision-strength score**, not a calibrated probability. - -## Why this boundary - -The historical v1 application materialized FIAS paths in Elasticsearch. That -made the parser, registry, storage engine, and deployment topology one system. -Version 2 separates those responsibilities: - -- this package tokenizes and extracts candidate address components; -- your application decides which results require review; -- your own current FIAS/GAR index or service verifies candidates and supplies - identifiers. - -No Elasticsearch, FIAS/GAR database, network call, service process, hidden -download, pandas, or scientific runtime is required by the package. The bundled -structured sequence model is about 37 KB. Large evaluation corpora stay outside -the wheel under the ignored `.cache/external/` directory. - -## Should I use this? - -| Your need | Fit | -| --- | --- | -| Extract Russian address fields in an offline Python process | **Yes** | -| Conservatively locate marked street/building spans in messages | **Yes** | -| Preserve raw substrings, offsets, warnings, and alternatives | **Yes** | -| Feed structured candidates into your own resolver | **Yes** | -| Verify current address existence or get a FIAS/GAR ID | **No—add a resolver** | -| Geocode, transliterate, or correct official spelling | **No** | -| Detect every implicit or markerless address in arbitrary prose | **No** | -| Require uniformly strong administrative-field extraction | **Not yet** | - -The current evidence supports conventional street/building extraction better -than isolated administrative names. Read the [benchmark boundaries](#reliability) -before setting automation policy. - -## Architecture - -```mermaid -flowchart LR - A["Address string"] --> C["Offset-preserving tokenizer"] - M["Free-form message"] --> B["Conservative span detector"] - B --> A - C --> D["Postcode and numeric grammar"] - D --> E["Explicit marker rules"] - E --> F["Tiny sequence tagger for residual text"] - F --> G["Deterministic post-processing"] - G --> H["ParsedAddress"] - H --> I["Application review policy"] - I --> J["Customer-managed FIAS/GAR resolver"] -``` - -Direct address strings can skip detection. The parser itself ends at -`ParsedAddress`; review and resolution remain application responsibilities. - -### What the ML stack actually is - -This is not a neural network, transformer, LLM, spaCy, scikit-learn, PyTorch, -or TensorFlow stack. It is a dependency-free hybrid: - -1. regular-expression tokenization and explicit address grammar; -2. marker and numeric extraction rules; -3. a 37 KB sparse linear-chain tagger for only the residual unmarked words; -4. Viterbi decoding and deterministic post-processing. - -The learned tagger is an epoch-averaged structured perceptron with six labels: -`O`, `REGION`, `DISTRICT`, `CITY`, `SETTLEMENT`, and `STREET`. Its JSON -artifact contains sparse lexical/context feature weights and transition -weights. Message detection is currently rule-based; FIAS/GAR lookup is an -optional downstream application stage. - -See [ML stack and runtime boundaries](docs/ml-stack.md) for the features, -training split, artifact details, and the recommended production separation -between detection, extraction, and registry resolution. - -## API reference - -The public package exports: +Каждый компонент содержит `value`, точную исходную подстроку `raw`, полуоткрытый +интервал `start:end`, источник решения и `confidence`. Весь результат можно +сериализовать: ```python -from address_normalizer import ( - AddressPart, - AddressPartDict, - Alternative, - AlternativeDict, - DetectedAddress, - DetectedAddressDict, - ParsedAddress, - ParsedAddressDict, - detect_addresses, - parse, - parse_iter, - parse_many, -) +payload = result.as_dict() # обычный JSON-совместимый dict ``` -### `parse(text: str) -> ParsedAddress` - -Parses one string without verifying it against a registry. Non-string input -raises `TypeError`. Empty or punctuation-only input returns an empty -`ParsedAddress`; decide at your application boundary whether that should be an -error. - -### `parse_many(addresses: Iterable[str]) -> list[ParsedAddress]` - -Consumes any iterable, preserves order, and returns a list. It is convenient -for bounded batches. - -### `parse_iter(addresses: Iterable[str]) -> Iterator[ParsedAddress]` - -Lazily consumes an iterable in order and keeps only one parsed result at a time. -Use it for unbounded files or streams, as shown in -[`examples/jsonl_etl.py`](https://github.com/shigabeev/address-normalizer/blob/master/examples/jsonl_etl.py). -`parse_many()` and `parse_iter()` reject a bare string so it cannot be mistaken -for a batch; a non-string element raises `TypeError` when iteration reaches it. - -### `detect_addresses(text: str) -> tuple[DetectedAddress, ...]` - -Locates conservative address candidates inside a free-form message. A candidate -normally needs a street marker plus a building number, or an explicit -`адрес:` cue plus a parseable street and building. This avoids treating every -place name or number as an address. +Неоднозначность не скрывается: ```python -from address_normalizer import detect_addresses - -message = ( - "Курьер приедет по адресу: Москва, ул. Тверская, " - "д. 13, кв. 4. Позвоните." -) - -for detected in detect_addresses(message): - assert message[detected.start:detected.end] == detected.text - print(detected.span, detected.text) - print(detected.parsed.as_dict()) -``` - -`DetectedAddress.span` indexes the original message. Component offsets inside -`DetectedAddress.parsed` index the extracted `DetectedAddress.text`. Detection -confidence is decision strength, not a probability or registry verification. -Multiple non-overlapping addresses are returned in message order. - -### Result types - -`ParsedAddress` can contain: +result = parse("Ополченская 5-30") -```text -postal_code, region, district, city, settlement, -street, street_type, house_num, corpus, structure, apartment, -unparsed, warnings, alternatives, confidence +print(result.normalized) # Ополченская, д 5, кв 30 +print(result.warnings) # ("ambiguous_numeric_tail",) +print(result.alternatives) # среди вариантов есть дом 5-30 ``` -Each `AddressPart` contains: - -- `value`: lightly normalized extracted value; -- `raw`: exact substring from the input; -- `start`, `end`, and `span`: original `[start, end)` character offsets; -- `confidence`: bounded decision-strength score; -- `source`: `rule`, `model`, `postprocessor`, or `unparsed`. - -`ParsedAddress.as_dict()` returns a `ParsedAddressDict` made of JSON-compatible -built-in values; `AddressPartDict` and `AlternativeDict` describe nested -objects. Its top level also contains `raw`, `normalized`, and overall -`confidence`. The dataclasses are frozen; treat this serialized shape and the -exported names as the v2 alpha contract. - -## Review policy - -Do not turn the overall confidence into a universal accept/reject threshold. -It has not been calibrated as a probability, and a score learned on one input -domain does not establish the error rate on another. - -A safe default policy is: +`confidence` — сила решения внутри парсера, а не вероятность существования +адреса. Результаты с `warnings`, `alternatives` или `unparsed` стоит проверять. -1. require expected business fields, such as `street` and `house_num`; -2. send any result with `warnings`, `alternatives`, or non-empty `unparsed` to - review or resolver-assisted disambiguation; -3. validate every selected part by slicing the original text with its `span`; -4. choose any confidence threshold only on a representative, labeled - validation set from your own traffic; -5. use a current customer-managed FIAS/GAR source to verify existence and - choose among candidates. +### Поиск адреса в сообщении ```python -from address_normalizer import ParsedAddress +from address_normalizer import detect_addresses +message = "Доставить по адресу: Москва, ул. Тверская, д. 13. Позвоните." -def needs_review(result: ParsedAddress) -> bool: - required_fields_missing = result.street is None or result.house_num is None - unresolved_evidence = bool( - result.warnings or result.alternatives or result.unparsed - ) - return required_fields_missing or unresolved_evidence +for item in detect_addresses(message): + print(item.text) # Москва, ул. Тверская, д. 13 + print(item.span) # смещение в исходном сообщении + print(item.parsed) # ParsedAddress ``` -[`examples/fias_gar_http.py`](https://github.com/shigabeev/address-normalizer/blob/master/examples/fias_gar_http.py) -shows a deliberately generic HTTP boundary for a customer-managed resolver. -Adapt its request contract to your index; there is no canonical resolver API in -this package. - -## CLI reference +Детектор консервативный: лучше пропустить слабый кандидат, чем принять номер +заказа или дату за адрес. -Parse one positional address and emit an indented JSON object: - -```bash -address-normalizer "СПб Невский проспект 10 корп 2 кв 15" -``` +### Несколько адресов и CLI -With no positional address, the command reads one address from standard input: +```python +from address_normalizer import parse_many -```bash -printf '%s' 'Москва Тверская д 5/1 кв 9' | address-normalizer +results = parse_many(["Тверская 1", "Невский проспект 10"]) ``` -`--jsonl` reads one address per line and writes one compact JSON object per -line. Blank lines are parsed as empty addresses rather than skipped. - ```bash -printf '%s\n' \ - 'Ополченская 5-30' \ - 'Самара Авроры 7 12' | +address-normalizer "СПб, Невский проспект 10, корп. 2" +printf '%s\n' "Тверская 1" "Ополченская 5-30" | address-normalizer --jsonl ``` -Exit status is zero after successful processing. Invalid invocation is handled -by `argparse`; malformed content is represented in the parse result rather than -treated as a CLI syntax error. - -## Common recipes - -- [`examples/basic.py`](https://github.com/shigabeev/address-normalizer/blob/master/examples/basic.py): - typed single-address parsing and review routing; -- [`examples/jsonl_etl.py`](https://github.com/shigabeev/address-normalizer/blob/master/examples/jsonl_etl.py): - constant-memory JSONL ETL from standard input; -- [`examples/fastapi_app.py`](https://github.com/shigabeev/address-normalizer/blob/master/examples/fastapi_app.py): - an optional FastAPI wrapper without changing the package's runtime - dependencies; -- [`examples/fias_gar_http.py`](https://github.com/shigabeev/address-normalizer/blob/master/examples/fias_gar_http.py): - inspect or send a resolver request to an endpoint you control. - -These examples are integration starting points, not extra behavior hidden in -the core package. - -## Reliability - -There is no single “accuracy” number. The committed reports cover different -domains and matching rules, and the results must not be averaged: - -| Domain | Test size | Primary metric | Result | Important boundary | -| --- | ---: | --- | ---: | --- | -| Historical bank-shaped reference | 500 rows | exact component micro F1 | 95.9% | Silver regression data; not independently re-reviewed for v2 | -| RedMadRobot noisy address windows | 578 windows | same-label binary span-overlap F1 | 58.7% | Address windows are selected using gold annotations | -| Deepparse nationwide clean strings | 100,000 rows | same-label character-overlap F1 | 66.2% | Registry-derived clean strings with a different token schema | -| Moscow official clean buildings | 15,196 rows | exact component-value micro F1 | 85.4% | Moscow-only October 2021 snapshot | - -Message-span detection is measured separately. On complete RedMadRobot messages, -the current development diagnostic reports 98.0% any-overlap precision, 68.1% -recall, and 80.3% F1 for gold windows containing both `STREET` and `HOUSE`. -Those detector failures were inspected during development, so this is not a -sealed final-test result. Exact-boundary F1 is only 23.8% because gold and -detector boundary policies frequently disagree about surrounding city, -postcode, country, and marker text. - -Metric names matter: - -- **exact component micro F1** pools true/false positive and negative component - values across all scored fields, requiring normalized values to match; -- **binary span-overlap F1** counts a one-to-one same-label span as matched if - the character ranges overlap at all, so it is intentionally lenient; -- **character-overlap F1** scores the amount of correctly overlapping - same-label text and exposes partial or merged spans; -- **token-label F1** pools precision, recall, and F1 over labels aligned to the - source tokens; -- **exact full-address match** requires every scored component value in one row - to match and no extra scored component to be produced. - -Additional context prevents misleading comparisons: - -- RedMadRobot street F1 is 49.1%, while house F1 is 90.2%; -- Deepparse also reports 84.4% binary span-overlap F1 and 66.5% token-label F1; -- the Moscow report has 64.9% exact full-address match, 66.2% street F1, and - 98.2% house F1. - -See -[`evaluation/README.md`](https://github.com/shigabeev/address-normalizer/blob/master/evaluation/README.md) -for pinned sources, exact filters, per-field results, commands, and limitations. -[`evaluation/RESULTS.md`](https://github.com/shigabeev/address-normalizer/blob/master/evaluation/RESULTS.md) -indexes every committed benchmark report. -The historical regression gate is reproducible without downloading large -corpora: - -```bash -python evaluation/evaluate.py \ - --data evaluation/legacy_reference_500.jsonl \ - --gates evaluation/release_gates.json \ - --output evaluation/legacy_reference_500_report.json -``` - -The external benchmark commands require separately installed data-preparation -dependencies and downloads. They never become runtime dependencies. - -## Limitations - -- Parsing does not prove that an address exists or is current. -- No FIAS/GAR identifiers or coordinates are returned. -- Administrative-field recall and exact street boundaries are materially - weaker than numeric building fields on current external benchmarks. -- Hyphenated and unmarked numeric tails can remain genuinely ambiguous. -- Normalization is deliberately light; it is not official-spelling correction. -- Confidence is not a probability and has not been calibrated across domains. -- Detection deliberately misses unmarked address-like text without an - `адрес:` cue and marked streets without a building. Its committed 30-message - fixture is a behavior regression set, not a production accuracy benchmark. -- The current package version is an alpha; public API and model behavior may - still change before the beta. - -## Migrating from v1 - -Version 2 is a new product boundary, not a drop-in replacement for the root -v1 Elasticsearch application. +## Граница ответственности -| v1 responsibility | v2 approach | -| --- | --- | -| Imports from root `api.py` / `parsing.py` | Import `parse` from `address_normalizer` | -| FIAS data loaded into Elasticsearch | Operate your registry/index separately | -| Resolver-shaped final result | `ParsedAddress` extraction candidates | -| Corrected or registry-backed values | Lightly normalized values from source text | -| Implicit resolution choice | Explicit `warnings`, `alternatives`, and `unparsed` | -| Service/application deployment | Library API or JSONL CLI | +Библиотека извлекает: -During migration, keep v1 resolution and v2 extraction side by side on recorded -traffic, compare by field, and define domain-specific review gates before -switching writes. Do not compare v1 registry correction with v2 extraction as -if they were the same task. +- индекс, регион, район, город и населённый пункт; +- улицу и тип улицы; +- дом, корпус, строение и квартиру; +- исходные смещения, неразобранный остаток, предупреждения и альтернативы. -The historical root files (`api.py`, `parsing.py`, `upload_fias.py`, -`docker-compose.yaml`, and `requirements-legacy.txt`) are retained as an -architectural record and are not included in the v2 wheel. +Она не проверяет адрес по ФИАС/ГАР, не возвращает ID реестра, не исправляет +официальное написание и не геокодирует. После парсинга передайте поля в свой +актуальный resolver ФИАС/ГАР. -## Troubleshooting +## Как это работает -**`pip install address-normalizer` does not provide this v2 API.** +Runtime — простой гибрид: токенизатор со смещениями, правила адресных маркеров и +чисел, компактный линейный sequence tagger для слов без маркеров, затем +детерминированная постобработка. Это не LLM и не нейросеть. Модель занимает +37 КБ; сетевых запросов и скрытых загрузок нет. -The v2 alpha has not been published. Install from this checkout until a release -is explicitly announced. +## Качество -**A FIAS ID is missing.** +Одна цифра «accuracy» здесь вводит в заблуждение, поэтому разные наборы +публикуются отдельно: -That is expected. Send selected fields to a current resolver you operate; see -the FIAS/GAR HTTP example. +| Набор | Размер | Метрика | Результат | +| --- | ---: | --- | ---: | +| Историческая адресная выборка | 500 | exact component micro F1 | 95,9% | +| Шумные адресные фрагменты | 578 | span-overlap F1 | 58,7% | +| Чистые адреса по России | 100 000 | character-overlap F1 | 66,2% | +| Здания Москвы | 15 196 | exact component micro F1 | 85,4% | -**A high-confidence parse is wrong.** +Подробные определения, результаты по полям и все 500 строк с причинами ошибок: +[`benchmarks/README.md`](https://github.com/shigabeev/address-normalizer/blob/master/benchmarks/README.md). -Confidence expresses parser decision strength, not correctness probability. -Capture the exact input, output, expected fields, and business context in a -[parsing-failure report](https://github.com/shigabeev/address-normalizer/issues/new?template=parsing-failure.yml). +Сильные поля — дом, корпус и строение. Слабее — административные уровни, точная +граница улицы, редкие сокращения и числовые хвосты без маркеров. -**`parse_many()` uses too much memory.** - -It returns a list by contract. Stream records through `parse_iter()` or use the -JSONL CLI/ETL example. - -**Offsets appear wrong after normalization.** - -Offsets index the original `result.raw`, not `result.normalized`. Verify with -`result.raw[part.start:part.end] == part.raw`. - -**A line disappears or appears empty in JSONL output.** - -The CLI emits one result per input line and intentionally keeps blank lines. -Filter blank records in the calling pipeline if that is the desired policy. - -**The parser imports but the bundled model cannot be found.** - -Install the built wheel rather than copying the package directory manually, and -include the exact install command and wheel contents in a bug report. - -## Development +## Разработка ```bash python -m pip install -e . +python -m pip install pytest pytest -python training/train_compact_tagger.py -python training/evaluate_compact_tagger.py -``` - -Large external data preparation has separate, pinned tooling: - -```bash -python -m pip install -r requirements-evaluation.txt -python evaluation/prepare_deepparse.py --download -python evaluation/prepare_datamos.py --download -``` - -Build artifacts: - -```bash -python -m pip install build -python -m build +python tools/benchmark.py --check ``` -Read -[`CONTRIBUTING.md`](https://github.com/shigabeev/address-normalizer/blob/master/CONTRIBUTING.md) -before proposing a change. Bug reports, minimal parsing failures, provenance -information, documentation corrections, and focused pull requests are welcome. +Публичный API находится в `src/address_normalizer`, основные проверки — в +`tests`, воспроизводимый набор ошибок — в `benchmarks`. -## License +## Лицензия -GNU General Public License v3.0 only (`GPL-3.0-only`). See -[`LICENSE`](https://github.com/shigabeev/address-normalizer/blob/master/LICENSE) -and the -[`LICENSING.md`](https://github.com/shigabeev/address-normalizer/blob/master/LICENSING.md) -provenance record. +GNU GPL v3.0 only. Полный текст — в +[`LICENSE`](https://github.com/shigabeev/address-normalizer/blob/master/LICENSE). diff --git a/README.ru.md b/README.ru.md deleted file mode 100644 index 56d7855..0000000 --- a/README.ru.md +++ /dev/null @@ -1,396 +0,0 @@ -# address-normalizer - -**Извлекает типизированные поля из неструктурированного российского адреса, -сохраняет исходные смещения и работает локально — без runtime-зависимостей и -скачивания справочника.** - -[English version](https://github.com/shigabeev/address-normalizer/blob/master/README.md) - -`address-normalizer` v2 — небольшая Python-библиотека для приложений, которые -используют или планируют использовать собственный поиск по ФИАС/ГАР. Она -извлекает компоненты адреса, но не подтверждает существование адреса, не -возвращает идентификатор ФИАС, не геокодирует и ничего не скачивает скрытно. - -## Быстрый старт за 30 секунд - -```bash -python -m pip install address-normalizer -address-normalizer "Ополченская 5-30" -``` - -Типизированный Python API: - -```python -from address_normalizer import parse - -result = parse("Ополченская 5-30") - -print(result.normalized) -# Ополченская, д 5, кв 30 -``` - -Эта короткая запись неоднозначна. Парсер сохраняет исходные смещения, выбранную -интерпретацию, предупреждение и правдоподобную альтернативу с составным номером -дома: - -```json -{ - "normalized": "Ополченская, д 5, кв 30", - "street": { - "value": "Ополченская", - "raw": "Ополченская", - "span": [0, 11], - "confidence": 0.78, - "source": "model" - }, - "house_num": {"value": "5", "span": [12, 13]}, - "apartment": {"value": "30", "span": [14, 16]}, - "warnings": ["ambiguous_numeric_tail"], - "alternatives": [ - { - "components": {"house_num": "5-30"}, - "reason": "A hyphenated numeric tail can also be a compound house number." - } - ] -} -``` - -Полная стабильная форма словаря доступна через `result.as_dict()`. Пример выше -сокращён, чтобы показать принятое решение. `confidence` — ограниченный показатель -силы решения, а не калиброванная вероятность. - -## Почему граница продукта проходит здесь - -Историческое приложение v1 материализовывало пути ФИАС в Elasticsearch. В -результате парсер, реестр, хранилище и схема развёртывания становились одной -системой. Версия 2 разделяет обязанности: - -- библиотека токенизирует строку и извлекает компоненты-кандидаты; -- приложение решает, какие результаты требуют проверки; -- актуальный индекс или сервис ФИАС/ГАР подтверждает кандидатов и возвращает - идентификаторы. - -Библиотеке не нужны Elasticsearch, база ФИАС/ГАР, сетевые запросы, сервисный -процесс, скрытая загрузка, pandas или научный Python-стек. Встроенная -структурная модель занимает около 37 КБ. Большие оценочные корпуса остаются вне -wheel в игнорируемом каталоге `.cache/external/`. - -## Подходит ли библиотека для моей задачи? - -| Задача | Подходит | -| --- | --- | -| Извлечь поля российского адреса в локальном Python-процессе | **Да** | -| Консервативно найти размеченную улицу и дом внутри сообщения | **Да** | -| Сохранить исходные подстроки, смещения, предупреждения и альтернативы | **Да** | -| Передать структурированных кандидатов собственному resolver | **Да** | -| Проверить существование адреса или получить ID ФИАС/ГАР | **Нет — нужен resolver** | -| Геокодировать, транслитерировать или исправить официальное написание | **Нет** | -| Найти каждый неявный адрес без маркеров в произвольном тексте | **Нет** | -| Одинаково хорошо извлекать все административные уровни | **Пока нет** | - -Текущие результаты лучше подтверждают обычные адреса с улицей и домом, чем -изолированные административные названия. Перед автоматизацией прочитайте -[границы измерений](#надёжность). - -## Архитектура - -```mermaid -flowchart LR - A["Строка адреса"] --> C["Токенизатор со смещениями"] - M["Свободное сообщение"] --> B["Консервативный детектор"] - B --> A - C --> D["Индекс и числовая грамматика"] - D --> E["Правила явных маркеров"] - E --> F["Компактный теггер остаточного текста"] - F --> G["Детерминированная постобработка"] - G --> H["ParsedAddress"] - H --> I["Политика проверки приложения"] - I --> J["Resolver ФИАС/ГАР пользователя"] -``` - -Готовая строка адреса может миновать детектор. Сам парсер заканчивает работу на -`ParsedAddress`; проверка и разрешение остаются обязанностью приложения. - -### Что здесь действительно относится к ML - -Это не нейросеть, transformer, LLM, spaCy, scikit-learn, PyTorch или -TensorFlow. Это dependency-free гибрид: - -1. токенизация регулярными выражениями и явная адресная грамматика; -2. правила для маркеров и числовых компонентов; -3. разреженный линейно-цепочечный теггер размером 37 КБ только для оставшихся - слов без маркеров; -4. декодирование Витерби и детерминированная постобработка. - -Обучаемый теггер — усреднённый по эпохам структурный перцептрон с метками `O`, -`REGION`, `DISTRICT`, `CITY`, `SETTLEMENT` и `STREET`. JSON-артефакт содержит -разреженные веса лексических/контекстных признаков и переходов. Детектор -сообщений сейчас основан на правилах; ФИАС/ГАР — необязательный следующий этап -в приложении. - -Подробности: [ML-стек и границы runtime](docs/ml-stack.md). - -## API - -Публичные экспорты: - -```python -from address_normalizer import ( - AddressPart, - AddressPartDict, - Alternative, - AlternativeDict, - DetectedAddress, - DetectedAddressDict, - ParsedAddress, - ParsedAddressDict, - detect_addresses, - parse, - parse_iter, - parse_many, -) -``` - -### `parse(text: str) -> ParsedAddress` - -Разбирает одну строку без проверки по реестру. Для значения не типа `str` -выбрасывает `TypeError`. Пустая строка или строка только из пунктуации возвращает -пустой `ParsedAddress`; приложение само решает, считать ли это ошибкой. - -### `parse_many(addresses: Iterable[str]) -> list[ParsedAddress]` - -Обрабатывает любой конечный iterable, сохраняет порядок и возвращает список. - -### `parse_iter(addresses: Iterable[str]) -> Iterator[ParsedAddress]` - -Лениво обрабатывает iterable по порядку и держит в памяти один результат. -Используйте для файлов и потоков без фиксированного размера. `parse_many()` и -`parse_iter()` отвергают одиночную строку, чтобы не принять её за набор -адресов. - -### `detect_addresses(text: str) -> tuple[DetectedAddress, ...]` - -Консервативно находит адреса в свободном сообщении. Обычно кандидату нужен -маркер улицы вместе с номером дома либо явный префикс `адрес:` и разбираемые -улица/дом. - -```python -from address_normalizer import detect_addresses - -message = ( - "Курьер приедет по адресу: Москва, ул. Тверская, " - "д. 13, кв. 4. Позвоните." -) - -for detected in detect_addresses(message): - assert message[detected.start:detected.end] == detected.text - print(detected.span, detected.text) - print(detected.parsed.as_dict()) -``` - -`DetectedAddress.span` индексирует исходное сообщение. Смещения компонентов в -`DetectedAddress.parsed` относятся к извлечённому `DetectedAddress.text`. -Несколько непересекающихся адресов возвращаются в порядке появления. - -### Результаты - -`ParsedAddress` может содержать: - -```text -postal_code, region, district, city, settlement, -street, street_type, house_num, corpus, structure, apartment, -unparsed, warnings, alternatives, confidence -``` - -Каждый `AddressPart` содержит: - -- `value` — слегка нормализованное значение; -- `raw` — точную подстроку входа; -- `start`, `end`, `span` — полуоткрытые смещения `[start, end)` в исходной - строке; -- `confidence` — силу решения; -- `source` — `rule`, `model`, `postprocessor` или `unparsed`. - -`ParsedAddress.as_dict()` возвращает JSON-совместимый `ParsedAddressDict`. -Датаклассы immutable; экспортированные имена и сериализованная структура — -контракт альфа-версии v2. - -## Политика проверки результата - -Не используйте общую `confidence` как универсальный порог принятия. Это не -вероятность, и она не устанавливает частоту ошибок на другом домене. - -Безопасная начальная политика: - -1. требовать бизнес-поля, например `street` и `house_num`; -2. отправлять результат с `warnings`, `alternatives` или `unparsed` на ручную - либо реестровую проверку; -3. проверять каждую часть срезом исходной строки по `span`; -4. выбирать порог только на размеченной выборке своего трафика; -5. использовать актуальный ФИАС/ГАР для подтверждения существования и выбора - кандидата. - -```python -from address_normalizer import ParsedAddress - - -def needs_review(result: ParsedAddress) -> bool: - required_fields_missing = result.street is None or result.house_num is None - unresolved_evidence = bool( - result.warnings or result.alternatives or result.unparsed - ) - return required_fields_missing or unresolved_evidence -``` - -Пример [`examples/fias_gar_http.py`](examples/fias_gar_http.py) показывает -обобщённую HTTP-границу для resolver, которым управляет пользователь. - -## CLI - -Один адрес: - -```bash -address-normalizer "СПб Невский проспект 10 корп 2 кв 15" -``` - -Без позиционного аргумента команда читает адрес из stdin: - -```bash -printf '%s' 'Москва Тверская д 5/1 кв 9' | address-normalizer -``` - -Режим `--jsonl` читает одну строку адреса и пишет один компактный JSON на строку: - -```bash -printf '%s\n' \ - 'Ополченская 5-30' \ - 'Самара Авроры 7 12' | - address-normalizer --jsonl -``` - -Пустые строки намеренно сохраняются как пустые результаты. - -## Готовые примеры - -- [`examples/basic.py`](examples/basic.py) — один адрес и маршрутизация на - проверку; -- [`examples/detect_in_message.py`](examples/detect_in_message.py) — поиск - адресов внутри сообщения; -- [`examples/jsonl_etl.py`](examples/jsonl_etl.py) — потоковый JSONL ETL с - постоянным объёмом памяти; -- [`examples/fastapi_app.py`](examples/fastapi_app.py) — необязательная FastAPI - обёртка; -- [`examples/fias_gar_http.py`](examples/fias_gar_http.py) — формирование - запроса к собственному resolver ФИАС/ГАР. - -## Надёжность - -Единого числа «accuracy» нет. Отчёты используют разные домены и правила -сопоставления; усреднять их нельзя: - -| Домен | Размер теста | Основная метрика | Результат | Важная граница | -| --- | ---: | --- | ---: | --- | -| Исторический банковский reference | 500 | micro F1 точных компонентов | 95,9% | Silver regression, не новый gold benchmark | -| Шумные окна RedMadRobot | 578 | F1 пересечения span одинакового типа | 58,7% | Окна выбраны с помощью gold-разметки | -| Чистые адреса Deepparse по России | 100 000 | character-overlap F1 | 66,2% | Реестровые строки с другой схемой токенов | -| Официальные здания Москвы | 15 196 | micro F1 точных компонентов | 85,4% | Москва, снимок октября 2021 года | - -Детекция сообщений измеряется отдельно. На полных сообщениях RedMadRobot -диагностика показывает 98,0% precision, 68,1% recall и 80,3% F1 по любому -пересечению для окон с `STREET` и `HOUSE`. Это development diagnostic, а не -независимый финальный тест. Exact-boundary F1 равен 23,8%, потому что gold и -детектор по-разному включают город, индекс, страну и маркеры. - -Дополнительные результаты: - -- исторический тест: 80,4% полностью точных адресов и 76,8% без остатка; -- RedMadRobot: улица 49,1% F1, дом 90,2% F1; -- Deepparse: 84,4% binary span-overlap F1 и 66,5% token-label F1; -- Москва: 64,9% полностью точных адресов, улица 66,2% F1, дом 98,2% F1. - -Полные определения, источники, фильтры, поля и ограничения находятся в -[`evaluation/README.md`](evaluation/README.md), а все сохранённые результаты -проиндексированы в [`evaluation/RESULTS.md`](evaluation/RESULTS.md). - -Портативный regression gate: - -```bash -python evaluation/evaluate.py \ - --data evaluation/legacy_reference_500.jsonl \ - --gates evaluation/release_gates.json \ - --output evaluation/legacy_reference_500_report.json -``` - -## Ограничения - -- Парсинг не доказывает, что адрес существует или актуален. -- Библиотека не возвращает идентификаторы ФИАС/ГАР и координаты. -- Административные поля и точные границы улиц слабее числовых полей здания. -- Дефисные и немаркированные числовые хвосты могут быть неоднозначны. -- Нормализация намеренно лёгкая и не исправляет официальное написание. -- `confidence` не является вероятностью и не калибрована между доменами. -- Детектор намеренно пропускает текст без маркеров/`адрес:` и улицу без дома. -- Версия остаётся alpha; API и поведение модели могут измениться до beta. - -## Миграция с v1 - -Версия 2 задаёт новую границу продукта и не является drop-in replacement для -старого Elasticsearch-приложения: - -| Обязанность v1 | Подход v2 | -| --- | --- | -| Импорты из корневых `api.py` / `parsing.py` | `from address_normalizer import parse` | -| Загрузка ФИАС в Elasticsearch | Отдельный реестр/индекс приложения | -| Готовый результат resolver | Кандидаты в `ParsedAddress` | -| Исправленные значения из реестра | Лёгкая нормализация исходного текста | -| Неявный выбор разрешения | Явные `warnings`, `alternatives`, `unparsed` | -| Развёртывание приложения | Библиотека или JSONL CLI | - -Старые корневые файлы сохранены как архитектурная история и не попадают в wheel. - -## Частые вопросы - -**Почему нет ID ФИАС?** -Это ожидаемо: передайте выбранные поля актуальному resolver, которым вы -управляете. - -**Почему результат с высокой confidence оказался неправильным?** -Confidence означает силу решения парсера, а не вероятность правильности. -Приложите синтетический пример через форму parsing failure. - -**Почему `parse_many()` использует много памяти?** -По контракту он возвращает список. Для потоков используйте `parse_iter()` или -JSONL CLI. - -**Почему смещения выглядят неправильно после нормализации?** -Они индексируют `result.raw`, а не `result.normalized`: -`result.raw[part.start:part.end] == part.raw`. - -**Почему строка исчезает или остаётся пустой в JSONL?** -CLI выдаёт результат на каждую входную строку, включая пустую. Фильтрацию -выполняет вызывающий процесс. - -## Разработка - -```bash -python -m pip install -e . -pytest -python training/train_compact_tagger.py -python training/evaluate_compact_tagger.py -``` - -Подготовка больших внешних данных имеет отдельные зависимости: - -```bash -python -m pip install -r requirements-evaluation.txt -python evaluation/prepare_deepparse.py --download -python evaluation/prepare_datamos.py --download -``` - -Перед pull request прочитайте [`CONTRIBUTING.md`](CONTRIBUTING.md). - -## Лицензия - -GNU General Public License v3.0 only (`GPL-3.0-only`). См. -[`LICENSE`](LICENSE) и запись о происхождении данных/модели в -[`LICENSING.md`](LICENSING.md). diff --git a/SECURITY.md b/SECURITY.md index 5a0b1fd..04da5a0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,57 +1,16 @@ -# Security policy +# Security -## Supported versions +Version `2.0.0a2` is the currently supported alpha. -| Version | Supported | -| --- | --- | -| Latest 2.0 pre-release | Yes, best effort | -| Historical v1 Elasticsearch application | No | +Please report vulnerabilities through +[GitHub private vulnerability reporting](https://github.com/shigabeev/address-normalizer/security/advisories/new). +Do not include private addresses, credentials, or customer data in a public +issue. -The alpha does not yet have a guaranteed security response or maintenance SLA. +Useful reports include the affected version, a minimal synthetic reproduction, +impact, and any suggested mitigation. -## Report privately - -Do not open a public issue for a suspected vulnerability or include private -addresses, credentials, tokens, or exploit details in public artifacts. - -Use GitHub private vulnerability reporting for this repository if the -**Report a vulnerability** option is available under the Security tab. Include: - -- affected version, commit, and installation method; -- impact and realistic attack conditions; -- minimal reproduction or proof of concept; -- whether secrets, filesystem access, network access, or untrusted package/model - data are involved; -- a safe way to contact you. - -If private vulnerability reporting is unavailable, use the -[security-contact request](https://github.com/shigabeev/address-normalizer/issues/new?template=security-contact.yml). -It asks only for a private channel and must contain no vulnerability details. -A maintainer can then arrange a private channel. This is a routing fallback, -not a place to disclose the vulnerability. - -The project cannot promise a response SLA before a maintainer security contact -and release process are formally established. The reporter should expect an -acknowledgment, impact assessment, coordinated fix, and disclosure timing to be -agreed before publication. - -## Security boundaries - -The v2 runtime is intended to: - -- parse caller-provided text without network access; -- make no filesystem writes during parsing; -- load only its bundled small model; -- require no runtime dependency; -- preserve rather than execute unparsed input. - -Changes to packaging, resource loading, training/evaluation data, generated -models, GitHub Actions, build provenance, and release credentials are -security-sensitive. Pull-request workflows must remain unprivileged and must -not execute contributor code in a privileged `pull_request_target` context. - -Incorrect address extraction is normally a correctness issue, not a -vulnerability. Treat it as security-sensitive when it crosses a trust boundary -or can lead to authorization bypass, unsafe file/network access, secret -exposure, code execution, or a practical denial of service. Otherwise use the -parsing-failure template and redact personal data. +The package is designed to run offline with no runtime dependencies. A network +request, hidden download, filesystem write, or process launch during parsing is +a security bug. Incorrect parsing is usually a correctness issue unless it +crosses a trust boundary or causes unsafe authorization, routing, or disclosure. diff --git a/SUPPORT.md b/SUPPORT.md deleted file mode 100644 index d3ac38d..0000000 --- a/SUPPORT.md +++ /dev/null @@ -1,36 +0,0 @@ -# Support - -`address-normalizer` v2 is an alpha maintained on a best-effort basis. There is -no paid support channel or guaranteed response time. - -Before opening an issue: - -1. read the README troubleshooting and product-boundary sections; -2. reproduce on the current v2 commit; -3. reduce address data to a synthetic or safely redacted example; -4. include package version, Python version, installation source, command, full - output, and expected behavior. - -Use the matching GitHub issue form for bugs, parsing failures, feature requests, -or data provenance. Public issues are the support record; do not send private -addresses, production logs, credentials, or large corpora. - -The project can help explain: - -- documented Python and CLI behavior; -- reproducible installation or packaging failures; -- unexpected parsing fields, offsets, warnings, and alternatives; -- evaluation commands and committed metric definitions; -- whether a proposed feature fits the small offline parser boundary. - -The project cannot provide: - -- a current FIAS/GAR database, identifier, or verification result; -- support for a customer-managed resolver, search cluster, or geocoder; -- legal advice about address data or licensing; -- private application debugging or integration consulting; -- guarantees that an alpha result is suitable for an automated business - decision. - -Security-sensitive reports follow [`SECURITY.md`](SECURITY.md) and must not be -disclosed in a public issue. diff --git a/api.py b/api.py deleted file mode 100644 index aa8d420..0000000 --- a/api.py +++ /dev/null @@ -1,159 +0,0 @@ -import pandas as pd -from elasticsearch import Elasticsearch - -from parsing import optimize_for_search, optimize_housenum, preprocess, extract_index, extract_house - -''' -Не работает без Elastic с загруженным туда ФИАС и проиндексированным на поиск родителей каждой строки -''' -es = Elasticsearch() - - -def verify_address(full_address): - ''' - Ищет адрес в ФИАС - Вход: строка - Выход: словарь с полным адресом по ФИАС и его составляющими - ''' - if full_address == '': - return [] - string = optimize_for_search(full_address) - query = { - 'size': 1, - "query": { - "query_string": { - "fields": ["fullname"], - "query": string, - "fuzziness": "auto", - #"use_dis_max": "true" - # "tie_breaker": 0.3 - } - } - } - response = es.search(index='fias_full_text', doc_type='address', body=query) - - if False: # True чтобы добавить в ответ текст запроса - dic = response["hits"]["hits"][0]["_source"] - dic['query'] = string - return dic - - try: - return response["hits"]["hits"][0]["_source"] - except IndexError: # Если не найдено - return [] - - -def verify_home(dic, aoguid, index): - ''' - Ищет конкретный дом на указанной улице - ''' - query = { - "size": 1, - "query": { - "bool": { - "must": [], - "should": [], ## Here goes your stuff - "must_not": [] - } - } - } - cases = query['query']['bool']['should'] - must = query['query']['bool']['must'] - must_not = query['query']['bool']['must_not'] - must.append({"match": { - "AOGUID": aoguid - }}) - if dic.get('дом', False): - must.append({"match": { - "HOUSENUM": optimize_housenum(dic["дом"]) - }}) - if dic.get('корпус', False): - cases.append({"match": { - "BUILDNUM": optimize_housenum(dic["корпус"]) - }}) - else: - must_not.append({"match": - {"BUILDNUM": "*"} - }) - if dic.get('строение', False): - cases.append({"match": { - "STRUCNUM": optimize_housenum(dic['строение']) - }}) - else: - must_not.append({"match": - {"STRUCNUM": '*'} - }) - if index: - cases.append({"match": { - "POSTALCODE": '"' + index + '"' - }}) - - must.append({"bool": { - - }}) - - response = es.search(index='fias_houses', body=query) - if len(response["hits"]["hits"]) == 0: - dic.update({"комментарий": "дом не найден в ФИАС"}) - return dic - else: - response = response["hits"]["hits"][0]["_source"] - - if response["BUILDNUM"] == response["HOUSENUM"]: - response['Корпус/строение'] = response["BUILDNUM"] - elif len(response["BUILDNUM"]) > len(response["HOUSENUM"]): - response['Корпус/строение'] = response["BUILDNUM"] - elif len(response["BUILDNUM"]) > len(response["HOUSENUM"]): - response['Корпус/строение'] = response["BUILDNUM"] - else: - response['Корпус/строение'] = response["BUILDNUM"] - new_dic = {key.lower(): response[key] for key in ["HOUSENUM", "BUILDNUM", "STRUCNUM", "POSTALCODE", "HOUSEID"]} - new_dic.update({'house query': str(query)}) - - return new_dic - - -def standardize(string, origin=True, debug=False): - ''' - Обёртка для всех методов выше. Разделяет адрес на его составляющие и ищет совпадение в ФИАС. В 90+% случаев находит. - Вход: строка с адресом - Выход: составляющие адреса - ''' - dic = {} - if origin: - dic['origin'] = string - - string = preprocess(string) - address, index = extract_index(string) - try: - index = index.strip() - except AttributeError: - pass - address, house = extract_house(address) - dic['index'] = index - dic['address'] = address - dic.update(verify_address(address)) - if dic.get('street', False): - dic.update(verify_home(house, dic['guid'], index)) - dic.update(house) - return dic - - -def get_addr(strings, progress=True): - ''' - Обрабатывает несколько адресов подряд. - Вход: массив строк - Выход: pandas Dataframe - ''' - dics = [] - n = len(strings) - 1 - for i, line in enumerate(strings): - if progress: - print("Working on {0} of {1}. Progress {2:03.1f}%".format(i, n, (i / n) * 100), end='\r') - dics.append(standardize(line)) - return pd.DataFrame(dics) - - -if __name__ == "__main__": - address = standardize("142703, Московская область, Ленинский район, г.Видное, ул. Школьная, д.78") - print(address['address'] + 'д ' + address['дом']) diff --git a/app.py b/app.py deleted file mode 100644 index 4b5cbe9..0000000 --- a/app.py +++ /dev/null @@ -1,45 +0,0 @@ -import api - -addr = "188640, ЛЕНИНГРАДСКАЯ, ВСЕВОЛОЖСКИЙ, СНТ. ТАВРЫ, Д. 422" -norm_addr = api.standardize(addr, True) -print(norm_addr) - -addr = "РОССИЯ,197373,г. Санкт-Петербург,РАЙОН ПРИМОРСКИЙ,Город САНКТ-ПЕТЕРБУРГ,,Проспект ШУВАЛОВСКИЙ,д. 59,кор. 1,,кв. 9" -norm_addr = api.standardize(addr, True) -print(norm_addr) - -addr = "182108, ВЕЛИКИЕ ЛУКИ, НОВЫЙ, Д.15 20" -norm_addr = api.standardize(addr, True) -print(norm_addr) - -addr = "660042, КРАСНОЯРСКИЙ КРАЙ, Г. КРАСНОЯРСК, УЛ. СВЕРДЛОВСКАЯ, Д. 61, КВ. 25" -norm_addr = api.standardize(addr, True) -print(norm_addr) - -addr = "157980, Костромская область, р-н. КАДЫЙСКИЙ Р-Н Поселок городского ти, , Улица БОЛЬНИЧНАЯ, д. 18, , кв. 1" -norm_addr = api.standardize(addr, True) -print(norm_addr) - -addr = "630000, Новосибирская область, г. НОВОСИБИРСК, , Улица АЛЕКСАНДРА-НЕВСКОГО, д. 6, , кв. 10" -norm_addr = api.standardize(addr, True) -print(norm_addr) - -addr = "190000, г. Санкт-Петербург, г. САНКТ-ПЕТЕРБУРГ, , Улица КУБИНСКАЯ, д. 28, , кв. 100" -norm_addr = api.standardize(addr, True) -print(norm_addr) - -addr = "115114, РОССИЯ, г Москва, Павелецкий 3-й проезд, д.6, корп.А, кв.58" -norm_addr = api.standardize(addr, True) -print(norm_addr) - -addr = "643,РОССИЯ,452920,02,БАШКОРТОСТАН РЕСП,,АГИДЕЛЬ Г,,СТУДЕНЧЕСКАЯ УЛ,14,,,20" -norm_addr = api.standardize(addr, True) -print(norm_addr) - -addr = "423330, 423330,РЕСПУБЛИКА ТАТАРСТАН,Г. АЗНАКАЕВО,,УЛИЦА ШАЙХУТДИНОВА,КВ. 2, Д. 9, ,, г. АЗНАКАЕВО, , , , ," -norm_addr = api.standardize(addr, True) -print(norm_addr) - -# addr = "москва, ленина ул. , д.6" -# norm_addr = api.standardize(addr, True) -# print(norm_addr) \ No newline at end of file diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..c5ab30b --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,88 @@ +# Benchmarks and known failures + +The parser extracts text; it does not resolve an address against FIAS/GAR. +Results from different datasets use different annotation and matching rules and +must not be averaged into one “accuracy” number. + +## Release results + +| Dataset | Rows/windows | Metric | Precision | Recall | F1 / rate | +| --- | ---: | --- | ---: | ---: | ---: | +| Historical address sample | 500 | exact component values, micro | 97.0% | 94.8% | **95.9%** | +| Historical address sample | 500 | every field exact | — | — | **80.4%** | +| Noisy address snippets | 578 | same-label span overlap | 60.0% | 57.5% | **58.7%** | +| Noisy street+house slice | 144 | same-label span overlap | 81.7% | 76.2% | **78.8%** | +| Clean nationwide addresses | 100,000 | character overlap | 72.8% | 60.8% | **66.2%** | +| Moscow buildings | 15,196 | exact component values, micro | 85.4% | 85.3% | **85.4%** | +| Free-message detector | 144 positives | span overlap | 98.0% | 68.1% | **80.3%** | +| Free-message detector | negative messages | specificity | — | — | **100%** | + +The detector’s specificity result belongs to its evaluated negative slice, not +to arbitrary production traffic. + +## Historical 500-row sample + +This is the only benchmark run in normal CI: + +```bash +python tools/benchmark.py --check +``` + +The command reads [`legacy_500.jsonl`](legacy_500.jsonl). +It passes when exact-address rate is at least 80%, exact-component micro F1 is +at least 95%, and at least 75% of rows have no residual word/number spans. + +Per-field exact-value results: + +| Field | Support | Precision | Recall | F1 | +| --- | ---: | ---: | ---: | ---: | +| postal code | 498 | 100.0% | 100.0% | 100.0% | +| region | 58 | 89.8% | 91.4% | 90.6% | +| district | 11 | 90.9% | 90.9% | 90.9% | +| city | 496 | 99.0% | 99.0% | 99.0% | +| settlement | 9 | 80.0% | 44.4% | 57.1% | +| street | 500 | 96.0% | 96.0% | 96.0% | +| street type | 500 | 94.8% | 87.8% | 91.2% | +| house | 500 | 95.8% | 95.0% | 95.4% | +| корпус | 75 | 100.0% | 92.0% | 95.8% | +| строение | 137 | 97.1% | 97.1% | 97.1% | +| apartment/unit | 95 | 96.2% | 80.0% | 87.4% | + +The sample contains 402 exact rows and 98 failures. The most common primary +failure hypotheses are: + +| Cause | Rows | +| --- | ---: | +| conflicting street markers | 22 | +| reference infers an absent street type | 20 | +| ambiguous or unsupported abbreviation | 15 | +| unmarked numeric roles | 14 | +| compound or letter-suffixed number boundary | 7 | +| administrative label/boundary | 6 | + +[`legacy_500_results.csv`](legacy_500_results.csv) +contains every row, +expected and actual values for every field, scenario flags, mismatch category, +unparsed spans, warnings, and a narrow failure hypothesis. These diagnoses are +triage aids, not independently adjudicated ground truth. + +## Interpreting the other datasets + +- The noisy snippet benchmark uses oracle-cropped address windows and + same-label span overlap. It is useful for messy input, not end-to-end message + detection. +- The nationwide set is large and clean but uses a schema adapter and lenient + character overlap. +- The Moscow set is official clean building data. House F1 is 98.2%, while + exact street F1 is 66.2%; this gap is more useful than its aggregate. +- The free-message benchmark measures conservative span detection. Exact-span + F1 is much lower than overlap F1, so callers must inspect returned offsets. + +The bundled 37 KB sequence model was trained with a deterministic, +group-disjoint 70/15/15 split of the historical rows. Its held-out token +accuracy is 96.5%, complete-sequence accuracy 94.3%, and micro entity F1 96.9%. +The split has little district and settlement coverage. + +The maintainer authored and authorized redistribution of the historical source +rows and derived model under GPL-3.0-only. External benchmark source corpora are +not included in this repository. diff --git a/evaluation/legacy_reference_500.jsonl b/benchmarks/legacy_500.jsonl similarity index 100% rename from evaluation/legacy_reference_500.jsonl rename to benchmarks/legacy_500.jsonl diff --git a/evaluation/legacy_reference_500_diagnostics.csv b/benchmarks/legacy_500_results.csv similarity index 100% rename from evaluation/legacy_reference_500_diagnostics.csv rename to benchmarks/legacy_500_results.csv diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 06041b1..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,91 +0,0 @@ -version: '2.2' -services: - es01: - image: docker.elastic.co/elasticsearch/elasticsearch:7.7.1 - container_name: es01 - restart: always - environment: - - node.name=es01 - - cluster.name=es-docker-cluster - - discovery.seed_hosts=es02,es03 - - cluster.initial_master_nodes=es01,es02,es03 - - bootstrap.memory_lock=true - - "ES_JAVA_OPTS=-Xms1g -Xmx1g" - ulimits: - memlock: - soft: -1 - hard: -1 - volumes: - - data01:/usr/share/elasticsearch/data - ports: - - 9200:9200 - networks: - - elastic - - es02: - image: docker.elastic.co/elasticsearch/elasticsearch:7.7.1 - container_name: es02 - restart: always - environment: - - node.name=es02 - - cluster.name=es-docker-cluster - - discovery.seed_hosts=es01,es03 - - cluster.initial_master_nodes=es01,es02,es03 - - bootstrap.memory_lock=true - - "ES_JAVA_OPTS=-Xms1g -Xmx1g" - ulimits: - memlock: - soft: -1 - hard: -1 - volumes: - - data02:/usr/share/elasticsearch/data - ports: - - 9201:9201 - networks: - - elastic - - es03: - image: docker.elastic.co/elasticsearch/elasticsearch:7.7.1 - container_name: es03 - restart: always - environment: - - node.name=es03 - - cluster.name=es-docker-cluster - - discovery.seed_hosts=es01,es02 - - cluster.initial_master_nodes=es01,es02,es03 - - bootstrap.memory_lock=true - - "ES_JAVA_OPTS=-Xms1g -Xmx1g" - ulimits: - memlock: - soft: -1 - hard: -1 - volumes: - - data03:/usr/share/elasticsearch/data - ports: - - 9202:9202 - networks: - - elastic - - kib01: - image: docker.elastic.co/kibana/kibana:7.7.1 - restart: always - container_name: kib01 - ports: - - 5601:5601 - environment: - ELASTICSEARCH_URL: http://es01:9200 - ELASTICSEARCH_HOSTS: http://es01:9200 - networks: - - elastic - -volumes: - data01: - driver: local - data02: - driver: local - data03: - driver: local - -networks: - elastic: - driver: bridge diff --git a/docs/good-first-issues.md b/docs/good-first-issues.md deleted file mode 100644 index 496b5be..0000000 --- a/docs/good-first-issues.md +++ /dev/null @@ -1,116 +0,0 @@ -# Good first issue proposals - -These are issue drafts, not work already authorized. A maintainer should assign -an owner, confirm the acceptance criteria still match `main`, and create the -issue before adding `good first issue`. - -All proposals are ready for discussion under the repository's GPL-3.0-only -contribution terms. - -## Available now: minimal missing-marker failure set - -**Why it matters:** Current external reports show that unmarked administrative -and street fields are weaker than numeric fields. Small public reproductions -help describe that boundary without sharing production data. - -**Scope** - -- Create three synthetic Russian address inputs covering distinct missing-marker - patterns. -- For each, record `parse(...).as_dict()`, expected component values, exact - `[start, end)` offsets, and why the expected interpretation is unambiguous. -- Submit them in one parsing-failure issue; do not change code or tests. - -**Acceptance criteria** - -- No real person or private address is used. -- Examples are not copied from committed sealed-test failure samples. -- Every expected `raw` value equals the source slice at its proposed span. -- The three examples represent different patterns, not spelling variants. - -## Available now: resolver contract field review - -**Why it matters:** The generic resolver example must be understandable to teams -that operate different FIAS/GAR indexes. - -**Scope** - -- Run `examples/fias_gar_http.py` in dry-run mode on two synthetic addresses, - including one with an alternative. -- Review whether `raw`, `components`, `warnings`, and `alternatives` are enough - to adapt at an application boundary. -- Open one documentation issue with confusing names or missing explanation; do - not propose a universal FIAS/GAR API. - -**Acceptance criteria** - -- No network request or private endpoint is used. -- The issue distinguishes parser output from resolver verification. -- Suggestions do not add FIAS/GAR data, authentication policy, or a network - dependency to the package. - -## CLI stdin and JSONL contract tests - -**Why it matters:** The CLI is the smallest integration surface for shell and -batch users, but its line-preservation behavior should be executable -documentation. - -**Scope** - -- Add subprocess tests for one positional address, single-address stdin, JSONL - order, CRLF input, Unicode, and a blank JSONL line. -- Assert JSON structure and process exit status, not whitespace formatting - except where the CLI contract requires it. - -**Acceptance criteria** - -- Each test fails for a demonstrated contract break, not only a fabricated - internal change. -- Tests invoke the installed entry point or the documented module boundary. -- No network, temp data outside the test directory, or timing assertion. - -## Public typed-dictionary example check - -**Why it matters:** `as_dict()` is the JSON boundary and exports typed-dictionary -shapes. A small checked example can catch documentation drift. - -**Scope** - -- Add a type-check fixture assigning `parse(...).as_dict()` to - `ParsedAddressDict`. -- Exercise one optional component, one `AddressPartDict`, warnings, and - alternatives. -- Document the selected type checker and exact command. - -**Acceptance criteria** - -- No new runtime dependency. -- The type-check dependency remains development-only. -- The fixture checks public imports rather than private implementation types. -- The command is run in CI only after the maintainer agrees on the type-check - policy. - -## Span-verification recipe - -**Why it matters:** Consumers need a safe way to prove every extracted `raw` -substring maps back to the original input. - -**Scope** - -- Add a compact example that iterates every populated part plus `unparsed` and - asserts `result.raw[start:end] == part.raw`. -- Cover Unicode whitespace and `ё` without normalizing the source first. -- Link it from the README review policy. - -**Acceptance criteria** - -- Uses only the public API and standard library. -- Demonstrates original-text offsets, not offsets into `normalized`. -- Includes a runnable command and expected output. - -## Tasks that are not good first issues - -Do not label compact-model retraining, benchmark threshold changes, new runtime -dependencies, public API redesign, release workflows, license selection, or -FIAS/GAR resolution as starter tasks. They require maintainer decisions and -cross-domain review. diff --git a/docs/launch.md b/docs/launch.md deleted file mode 100644 index f7cec08..0000000 --- a/docs/launch.md +++ /dev/null @@ -1,264 +0,0 @@ -# Launch drafts and evidence plan - -**Status: release facts verified; destination-specific posts remain drafts.** - -The maintainer approved GPL-3.0-only licensing, the recorded reference-data and -model provenance, and publication of `2.0.0a2` on 2026-07-29. The drafts below -are ready for final editorial review after the public package and release URLs -resolve. Nothing in this file authorizes automated posting to third-party -channels. - -## One-line problem and solution - -> Extract typed fields and original offsets from messy Russian addresses in a -> small offline Python package—then verify them against the FIAS/GAR resolver -> you already control. - -This line deliberately says “extract,” not “validate,” “resolve,” or “geocode.” - -## Facts to verify immediately before launch - -- release tag and package version; -- public license and exact repository/path scope; -- PyPI and GitHub release URLs; -- Python support matrix; -- clean wheel install and CLI/API output; -- wheel and bundled-model byte sizes; -- runtime dependency list; -- all committed report checksums and baseline values; -- current limitations and unresolved data terms; -- release notes and migration boundary. - -Never copy an approximate size or score from an older draft into a release. - -## Draft: GitHub release - -### address-normalizer 2.0.0a2: a small offline Russian address parser - -`address-normalizer` v2 extracts typed address components while preserving raw -substrings, character offsets, warnings, unparsed text, and alternative -interpretations. - -```python -from address_normalizer import parse - -result = parse("Ополченская 5-30") -print(result.normalized) -# Ополченская, д 5, кв 30 -print(result.warnings) -# ("ambiguous_numeric_tail",) -``` - -What is intentionally outside the package: - -- no bundled FIAS/GAR database; -- no address verification, identifier lookup, or geocoding; -- no Elasticsearch or service process; -- no hidden downloads or network calls; -- no runtime dependencies. - -The wheel is `45,843` bytes and its bundled model is `37,130` bytes in this -release. Independent benchmark results are published by domain rather than -averaged in the -[README reliability table](https://github.com/shigabeev/address-normalizer#reliability). -Numeric building fields are currently stronger than administrative fields and -exact street extraction. - -Install: `python -m pip install --pre address-normalizer==2.0.0a2` - -Documentation: [English README](https://github.com/shigabeev/address-normalizer#readme) -and [Russian README](https://github.com/shigabeev/address-normalizer/blob/master/README.ru.md) - -Migration notes: [Changelog](https://github.com/shigabeev/address-normalizer/blob/master/CHANGELOG.md) - -License: GPL-3.0-only for the repository and distributed package. - -## Draft: Show HN - -**Title** - -> Show HN: address-normalizer – small offline parsing for messy Russian addresses - -**Text** - -I rebuilt an old Russian address project around a narrower boundary. The new -Python package extracts components and preserves source offsets, warnings, -unparsed text, and alternatives. It does not bundle FIAS/GAR, run -Elasticsearch, verify existence, or make network calls. - -The motivating example is `Ополченская 5-30`: the parser selects house `5` and -apartment `30`, but retains compound house `5-30` as an alternative for a -downstream resolver. - -The runtime has no dependencies; verified artifact sizes for this release are -`45,843` and `37,130` bytes. The README publishes four non-comparable -benchmark domains and their limitations instead of one headline “accuracy” -number. Current weaknesses are administrative recall and exact street -boundaries. - -I would value feedback on the typed result contract, ambiguity handling, and -the boundary between extraction and a customer-managed FIAS/GAR resolver: -https://github.com/shigabeev/address-normalizer - -## Draft: Habr - -**Заголовок** - -> Маленький офлайн-парсер российских адресов без встроенного ФИАС и Elasticsearch - -**Лид** - -Старый `address-normalizer` решал сразу две задачи: разбирал строку и искал -адрес в заранее загруженном ФИАС. В версии 2 граница уже: библиотека только -извлекает компоненты, сохраняет исходные смещения, неоднозначности и -неразобранный остаток. Проверка существования и выбор идентификатора остаются -за актуальным справочником пользователя. - -**План текста** - -1. Почему парсинг и разрешение адреса — разные задачи. -2. Разбор `Ополченская 5-30`: выбранный вариант и сохранённая альтернатива. -3. Конвейер: токенизация → правила → компактная модель → постобработка. -4. Типизированный API и JSONL без runtime-зависимостей. -5. Как передать поля в собственный ФИАС/ГАР-сервис. -6. Четыре отдельных бенчмарка и почему их нельзя усреднять. -7. Слабые места: административные поля, точные границы улиц, неоднозначные - числовые хвосты. -8. Размеры артефактов, воспроизводимая команда и планы после alpha. - -**Финал** - -Исходники и методика: https://github.com/shigabeev/address-normalizer. -Особенно полезны синтетические -примеры ошибок с ожидаемыми полями и смещениями; реальные частные адреса -публиковать не нужно. - -## Draft: Reddit / r/Python - -**Title** - -> address-normalizer v2: dependency-free, offline Russian address field extraction - -**Body** - -I released `2.0.0a2` of a small Python library that extracts Russian -address components and preserves original character spans. It is intentionally -not a FIAS/GAR database, validator, geocoder, or service. - -The runtime has no dependencies and makes no network calls. `parse`, -`parse_iter`, and `parse_many` return frozen typed results with warnings, -alternatives, and JSON-compatible serialization. The README includes a FastAPI -wrapper, streaming JSONL ETL, and a generic boundary for a resolver you operate. - -I have kept the evidence separated across historical regression, noisy address -windows, nationwide clean strings, and an official Moscow snapshot. The weakest -current areas are administrative fields and exact street extraction. - -Repository and reproducible numbers: https://github.com/shigabeev/address-normalizer - -Feedback on API ergonomics and failure reporting is welcome. Please use -synthetic or redacted addresses. - -## Draft: Telegram / LinkedIn - -> `Ополченская 5-30` — это дом 5, квартира 30 или дом 5-30? -> -> `address-normalizer` v2 разбирает российские адресные строки офлайн, сохраняет -> исходные смещения и не скрывает неоднозначность. Внутри нет ФИАС/ГАР, -> Elasticsearch, сетевых запросов и runtime-зависимостей: библиотека извлекает -> кандидатов, а актуальный справочник пользователя их проверяет. -> -> В README есть типизированный API, JSONL, FastAPI-пример, интеграционная граница -> с собственным resolver и четыре раздельных бенчмарка с ограничениями. -> -> https://github.com/shigabeev/address-normalizer/releases/tag/v2.0.0a2 - -Before using this short post, add the verified license and release status in the -linked page; do not let brevity conceal them. - -## Reproducible benchmark command - -The small committed regression requires no external dataset download: - -```bash -python evaluation/evaluate.py \ - --data evaluation/legacy_reference_500.jsonl \ - --gates evaluation/release_gates.json -``` - -Record the commit SHA, Python version, full JSON output, wall-clock environment, -and whether the gate passed. Describe it as a historical silver regression, not -an independent nationwide production score. - -The large and external benchmark commands live in `evaluation/README.md`. Run -them only in a separate environment with pinned data artifacts. Report all -defined metrics and per-field results; do not rerun a sealed set repeatedly -while tuning. - -## Alternative-comparison methodology - -A fair comparison starts by aligning product boundaries. - -1. **Classify the alternative.** Is it an extractor, address validator, - FIAS/GAR resolver, geocoder, tokenizer/model, or complete service? Do not - rank different tasks on one “accuracy” axis. -2. **Pin public versions.** Record package/service version, source revision, - configuration, model/data revision, date, Python/platform, and exact command. -3. **Use permitted, task-matched data.** Separate noisy user input, clean - registry strings, administrative-only text, and historical compatibility. - Prevent entity/building groups from leaking across splits. -4. **Map schemas in writing.** Publish every field mapping and unsupported - field. Do not score corrected registry values as extractor truth when those - values do not occur in the input. -5. **Report multiple exact metrics.** At minimum publish exact component values, - exact full-address rows, character/span behavior, per-field precision/recall/ - F1, abstentions/unparsed evidence, and failure slices when supported. -6. **Measure operations separately.** Report artifact/download size, runtime - dependencies, cold/warm latency or throughput, peak memory, required service - or registry, network behavior, and hardware. Never infer an unmeasured value. -7. **Preserve failure evidence.** Count extra fields, missing fields, - alternatives, and discarded text. Do not award a cleaner score for hiding - ambiguity. -8. **Invite correction.** Publish the harness, raw aggregate report, license/ - provenance notes, and a contact path. Label unavailable results - “not measured,” not zero. - -Before naming a competitor in public, verify its current official documentation -and reproduce the claim. Do not repeat marketing copy, old architecture sizes, -or license assumptions as fact. - -## Release demo / terminal recording plan - -Target length: 75–100 seconds, one continuous recording, no edits that hide -installation or network activity. - -1. Start in a new virtual environment with network disabled after the wheel is - already available locally. -2. Show the wheel filename and exact byte size. -3. Install the wheel from its local path and show that no runtime dependency is - installed. -4. Run `Ополченская 5-30`; point to spans, warning, and compound-house - alternative rather than only normalized text. -5. Pipe two lines through `address-normalizer --jsonl`. -6. Run the FIAS/GAR HTTP example without `--send` and explain that it prints an - application-owned resolver request but performs no network call. -7. Show the four-domain benchmark table and the administrative/street - limitations. -8. End on the install command, repository URL, license, and alpha status. - -Save the command transcript beside the recording. Do not show tokens, internal -resolver URLs, shell history, private addresses, local usernames, or unpublished -benchmark data. - -## Launch checklist - -- [x] The licensing and provenance blockers are resolved in writing. -- [ ] A release actually exists at every linked URL. -- [x] Placeholder markers are gone. -- [ ] Install, API, CLI, wheel, and benchmark commands were rerun from the tag. -- [x] Sizes and metrics match the candidate artifacts. -- [x] Limitations remain adjacent to the claims they qualify. -- [x] No post implies validation, FIAS ID lookup, geocoding, or calibrated - confidence. -- [ ] Maintainer approved each destination-specific draft. -- [x] Nothing has been posted by automation. diff --git a/docs/ml-stack.md b/docs/ml-stack.md deleted file mode 100644 index 60ba04e..0000000 --- a/docs/ml-stack.md +++ /dev/null @@ -1,118 +0,0 @@ -# ML stack and runtime boundaries - -## Short answer - -`address-normalizer` is a hybrid rules-and-ML extractor. It is not a -transformer, neural network, LLM, or registry-backed parser. - -The only learned runtime component is a 37,130-byte sparse linear-chain -sequence tagger. It labels residual word tokens after deterministic syntax has -already extracted explicit address components. Message-level address detection -is currently rule-based. - -The package has no runtime dependencies outside the Python standard library and -makes no network calls. - -## Runtime pipeline - -| Stage | Implementation | Learned? | Responsibility | -| --- | --- | --- | --- | -| Message detection | Regular expressions, clause boundaries, evidence scoring, parser validation | No | Find conservative street-and-building candidate spans in prose | -| Tokenization | Offset-preserving regular-expression tokenizer | No | Split words, numbers, and punctuation without losing source offsets | -| Explicit extraction | Marker dictionaries and numeric grammars | No | Extract postcode, region/city/street markers, house, корпус, строение, and apartment | -| Residual labeling | Sparse linear-chain sequence tagger with Viterbi decoding | Yes | Label remaining unmarked words as region, district, city, settlement, street, or other | -| Post-processing | Deterministic boundary and numeric-tail heuristics | No | Fill plausible implicit street/house fields and retain warnings or alternatives | -| Registry resolution | Not part of this package | N/A | Verify existence, resolve abbreviations in context, choose a FIAS/GAR object, and return canonical values | - -Direct address strings enter at tokenization. Free-form messages first pass -through detection; every accepted detection is then parsed by the same address -parser. - -## The learned model - -The runtime model is an epoch-averaged structured perceptron: - -- labels: `O`, `REGION`, `DISTRICT`, `CITY`, `SETTLEMENT`, `STREET`; -- representation: sparse emission and transition weights serialized as JSON; -- decoding: first-order Viterbi sequence decoding; -- token features: lowercased word, word/number/punctuation kind, one- and - two-character prefixes, one- to three-character suffixes, sequence position, - neighboring token text and kind, and digit length; -- artifact: `src/address_normalizer/data/model.json`; -- artifact size: 37,130 bytes; -- stored parameters: 1,240 emission weights and 39 transition weights; -- training algorithm and inference: Python standard library only. - -This is closest in spirit to a small CRF-style linear sequence model, but it is -trained with a structured perceptron objective. It does not calculate neural -embeddings, use pretrained language representations, or produce calibrated -probabilities. - -The model only sees residual word tokens that rules have not consumed. It -therefore does not learn house-number grammar, address detection in prose, or -registry identity. - -## Training data and split - -The current model corpus is derived from the 500-row historical reference: - -- 429 sequence examples from 108 canonical address groups; -- deterministic 70/15/15 group split using SHA-256; -- 301 training, 58 validation, and 70 test sequence examples; -- epoch count selected on validation only; -- final artifact retrained on the 359 train-plus-validation examples; -- 10 selected epochs and seed `2017`. - -The group split prevents variants of the same canonical administrative/street -identity from appearing across train and test. The final 70-example sequence -test has 115 tokens, so its 96.5% token accuracy and 96.9% entity F1 are useful -regression evidence but not a production-scale claim. It contains no -independently measured district or settlement tokens. - -The much larger Deepparse and Moscow datasets are evaluation sources and -potential future training material. They are not used by the current bundled -model. - -## What FIAS/GAR changes - -Text extraction and registry resolution are different tasks. - -The text `пр. Ленина` is locally ambiguous because `пр.` can abbreviate more -than one street type. A full location and building can nevertheless identify -one registry object. Likewise, whether `с. 1` or `4А` is a building/unit role -may be resolved by the set of valid objects at the rest of the address. - -The intended production boundary is therefore: - -1. detect an inclusive address span in a message; -2. extract source-faithful component candidates and offsets; -3. query a current FIAS/GAR index with all available context; -4. rank registry candidates and return the canonical object; -5. retain the original text and parser warnings for auditability. - -The parser should not be trained to fabricate a registry-backed value that is -absent from the text merely to match a historical reference. The resolver can -enrich or correct the extracted candidate because it has the missing registry -knowledge. - -## Current production limitation - -The message detector is deliberately high-precision and rule-based. Its -complete-message RedMadRobot diagnostic has 98.0% overlap precision but only -68.1% recall. That makes it suitable when false positives are expensive, but -not yet sufficient when production requires finding most implicit, malformed, -or markerless addresses. - -A higher-recall production system should add a separately trained -message-level address-span model on representative messages, while keeping -parsing and FIAS/GAR resolution as distinct measured stages: - -```text -message -> address-span detector -> component parser -> FIAS/GAR resolver -``` - -Each stage should have its own metric: span recall/precision, conditional -component accuracy, resolver top-k recall, and end-to-end business success. - -All committed benchmark outputs and their precise scopes are indexed in -[`evaluation/RESULTS.md`](../evaluation/RESULTS.md). diff --git a/docs/releasing.md b/docs/releasing.md deleted file mode 100644 index d766751..0000000 --- a/docs/releasing.md +++ /dev/null @@ -1,98 +0,0 @@ -# Release process - -The GPL-3.0-only license and compact-model provenance are recorded in -`LICENSING.md` and enforced by `release-policy.toml`. - -## Version strategy - -`src/address_normalizer/__init__.py` is the single version source. Setuptools -reads `__version__` into package metadata; do not add a second literal version -to `pyproject.toml`. - -The build gate uses the fixed `SOURCE_DATE_EPOCH` shown below. Setuptools already -produces a byte-identical wheel with that epoch; the gate also canonicalizes -sdist tar ownership, modes, ordering, timestamps, and gzip metadata before -requiring two byte-identical builds. Do not replace this with a hash comparison -that tolerates differences. - -Use PEP 440 versions and matching `v`-prefixed Git tags: - -| Stage | Package version | Tag | Intended use | -| --- | --- | --- | --- | -| Alpha | `2.0.0a2` | `v2.0.0a2` | API and model may still change | -| Beta | `2.0.0b1` | `v2.0.0b1` | Feature-complete external testing | -| Release candidate | `2.0.0rc1` | `v2.0.0rc1` | Stable API, release fixes only | -| Stable | `2.0.0` | `v2.0.0` | Supported public release | - -Development snapshots, if needed, use `.devN` and are not published. Increment -the pre-release number rather than replacing an existing artifact. PyPI files -are immutable release records. - -## Release checklist - -### Maintainer decisions - -- [x] Record the chosen license and its exact scope. -- [x] Review and record historical contribution provenance. -- [x] Confirm redistribution rights for the compact model and its source data. -- [x] Add the license file and PEP 639 `license`/`license-files` metadata. -- [x] Set both statuses in `release-policy.toml` to `approved` and record - repository-relative evidence files for each decision. -- [x] Change the CI license gate from `--expect-blocked` to - `--require-publishable`. - -### Version and evidence - -- [ ] Move relevant entries from `Unreleased` into a dated changelog section. -- [ ] Set `address_normalizer.__version__` to the intended PEP 440 version. -- [ ] Run all unit, type, deterministic-model, and fixed regression gates. -- [ ] Review each external benchmark separately; do not average domains or tune - against a sealed final split. -- [ ] Confirm documentation describes confidence as decision strength, not - calibrated probability. - -### Distribution - -Release tooling requires Python 3.12, matching GitHub Actions. This does not -change the package's Python 3.10+ runtime support. Run from a clean checkout: - -```bash -SOURCE_DATE_EPOCH=1704067200 \ - python scripts/build_reproducibly.py --output dist -python scripts/check_artifacts.py dist --write-manifest artifact-manifest.json -python scripts/test_artifact_policy.py dist -python -m twine check dist/*.whl dist/*.tar.gz -``` - -- [ ] Confirm the wheel has no runtime dependencies. -- [ ] Confirm the model is at most 64 KiB and the wheel at most 256 KiB. -- [ ] Inspect `artifact-manifest.json` checksums and source revision. -- [ ] Install the wheel into a clean virtual environment with - `--no-index --no-deps`. -- [ ] Run `scripts/smoke_installed.py` with isolated Python from outside the - checkout. -- [ ] Confirm the wheel and sdist contain no corpora, caches, notebooks, - training data, legacy files, credentials, or generated bytecode. - -### GitHub and PyPI - -- [ ] Configure the PyPI project to trust this repository's `release.yml` - workflow and its protected `pypi` environment. -- [ ] Require maintainer approval on the `pypi` environment. -- [ ] With explicit maintainer approval, create and push the signed tag matching - the package version (for example, `v2.0.0rc1`). -- [x] Enable publication only after all licensing gates pass. -- [ ] Run the workflow manually from the matching tag, first for `testpypi` and - then for `pypi`; the selected ref, entered version, checkout commit, and - wheel metadata must all match. -- [ ] Review the build artifact and provenance manifest before approving the - environment deployment. - -Never use a long-lived PyPI API token. The workflow grants `id-token: write` -only to the protected publish job and uses PyPI Trusted Publishing. - -## Rollback - -Published files cannot be replaced. If a release is wrong, yank it on PyPI, -document why in the changelog, fix forward with a new version, and preserve the -original checksums and provenance record. diff --git a/docs/triage.md b/docs/triage.md deleted file mode 100644 index 3881af6..0000000 --- a/docs/triage.md +++ /dev/null @@ -1,106 +0,0 @@ -# Issue and pull-request triage - -This guide makes review predictable without pretending maintainers have an SLA. -It is guidance for humans; no labels or repository settings are applied by -automation. - -## First response - -For each new issue: - -1. remove public secrets or personal address data from view and follow the - security/privacy route; -2. confirm it concerns v2 rather than the unsupported historical v1 service; -3. request the exact version, minimal command/input, current output, and expected - result if missing; -4. reproduce before labeling a parser behavior as a confirmed bug; -5. identify the domain: synthetic, noisy/user-entered, clean registry, - historical compatibility, or unknown; -6. check for a duplicate and link the canonical issue; -7. keep proposed implementation separate from the user problem. - -Parsing failures should preserve punctuation, Unicode, output spans, warnings, -alternatives, and unparsed content. Prefer synthetic minimal cases; never ask -for a private corpus dump in a public issue. - -## Suggested labels - -| Label | Suggested color | Use | -| --- | --- | --- | -| `needs-triage` | `D4C5F9` | Reproduction or ownership has not been established | -| `bug` | `D73A4A` | Confirmed behavior contradicts the documented contract | -| `parsing` | `B60205` | Component, span, warning, or ambiguity behavior | -| `cli` | `1D76DB` | Command-line and JSONL behavior | -| `api` | `0052CC` | Public Python API or serialized result | -| `documentation` | `0075CA` | User or contributor documentation | -| `evaluation` | `5319E7` | Metrics, gates, adapters, or benchmark reports | -| `data` | `7057FF` | Dataset or generated-data concern | -| `provenance` | `8A2BE2` | Source, terms, attribution, or redistribution evidence | -| `packaging` | `0E8A16` | Build, wheel, install, metadata, or compatibility | -| `security` | `B60205` | Public tracking only after private disclosure is safe | -| `good first issue` | `7057FF` | Bounded task with exact acceptance criteria and mentor | -| `help wanted` | `008672` | Maintainer has defined scope and will review work | -| `blocked: decision` | `FBCA04` | Explicit maintainer/product choice is required | -| `needs-reproduction` | `FEF2C0` | Report lacks a locally repeatable case | -| `needs-provenance` | `F9D0C4` | Data/model source or permission evidence is incomplete | -| `duplicate` | `CFD3D7` | Canonical issue is linked before closing | -| `wontfix` | `FFFFFF` | Intentionally outside scope, with reason documented | - -Do not use `good first issue` on vague refactors, broad parser improvement, -model retraining, release workflows, or tasks blocked by an unstated design -decision. - -## Parsing failure disposition - -- **Confirmed regression:** add `bug` and `parsing`; record the last known good - version if known. -- **Known limitation:** link the README limitation, retain the example if it - adds a meaningful input class, and avoid promising a fix. -- **Resolver responsibility:** explain that extraction does not verify existence - or choose a FIAS/GAR ID. -- **Ambiguous ground truth:** preserve alternatives; do not force a single label - merely to close the issue. -- **Private or licensed data:** remove it from public view and ask for a - synthetic reproduction. - -A public failure example should become a regression test only after provenance, -privacy, expected spans, and the licensing/contribution policy allow it. - -## Pull-request review - -For eligible changes, require: - -- a linked issue or complete reproduced bug; -- a test that demonstrates failure before and success afterward; -- the author's own explanation of behavior and maintenance implications; -- exact focused and full-suite results; -- before/after metrics for every relevant benchmark domain; -- model and wheel size deltas where relevant; -- preservation of offsets, ambiguity, warnings, alternatives, and unparsed - evidence; -- no hidden network, registry, service, data, or runtime-dependency expansion. - -An unexplained generated patch is not reviewable evidence. Ask the author to -reduce it and explain it; do not reverse-engineer a bulk submission on their -behalf. - -## Benchmark-resistant review - -Reject or redesign a change when it: - -- tunes against a sealed test while continuing to describe it as untouched; -- improves only a lenient overlap metric while exact value or another field - regresses; -- averages historical, noisy, nationwide-clean, and Moscow-clean domains; -- discards unparsed text or ambiguity to manufacture a cleaner score; -- moves data-preparation dependencies into the runtime; -- increases model/package size without a measured, justified tradeoff. - -Require a validation-driven decision and report every measured domain -separately, including rejected regressions. - -## Closing language - -Be direct about scope. A useful close explains which documented boundary applies, -links the canonical issue or recipe, and states what new evidence would justify -reopening. “Not planned” without an explanation is not sufficient. diff --git a/evaluation/DATA_SOURCES.md b/evaluation/DATA_SOURCES.md deleted file mode 100644 index 9256cac..0000000 --- a/evaluation/DATA_SOURCES.md +++ /dev/null @@ -1,83 +0,0 @@ -# External data sources - -These sources complement the historical workbook. None should be added to model -training until its role, license, and split policy are recorded. - -## 1. RedMadRobot Russian PII NER benchmark - -- Source: -- License: MIT -- Size: 2,841 sentences, including 1,252 location/address entity spans -- Useful subset: 493 rows, 578 address windows, 1,010 fields supported by this - parser -- Strength: manually BIO-annotated, production-log-shaped inputs, hard negatives -- Limitation: personal values are replaced; some examples are synthetic - document templates; the adapter uses gold annotations to crop address windows -- Status: integrated as an independent external regression benchmark - -The exact revision and file SHA-256 are pinned in -`evaluate_redmadrobot.py`. The source CSV is downloaded into `.cache/` and is -not committed or used for training. - -## 2. Deepparse worldwide addresses, Russia configuration - -- Source: -- License: CC BY 4.0 -- Pinned candidate revision: `cb61e5e49db87f8c3586b5494149f612460f8992` -- Russian shard: 13,152,918 annotated addresses; 371,595,309-byte Parquet file -- Fields: street number/name, unit, municipality, district, county, province, - postal code, and country -- Strength: nationwide scale and an independently defined token schema -- Limitation: curated from libpostal/open geographic data rather than raw user - input; punctuation is removed; there is no predefined sealed split -- Integrated filter: retain street/house/unit-bearing rows, validate all token - labels, deduplicate normalized text, map labels to package fields, and group - building identities before deterministic 90/5/5 splitting -- Result: 6,314,158 unique usable rows and a 100,000-row sealed test sample -- Status: integrated as a reproducible external corpus and clean-address - benchmark; the current compact model has not been trained on it - -The committed manifest pins the source and generated artifact checksums. Report -binary span, character-overlap, and token metrics separately from the -RedMadRobot real-input-shape benchmark. - -## 3. Moscow official address registry - -- Source package: - -- Published origin: Moscow open-data portal, dataset ID 60562, Department of - City Property -- Pinned snapshot: version 3.630, released 15 October 2021 -- Size: 440,399 records; 121,581,813-byte download archive -- Fields include full and simplified address strings plus region, city, - settlement, street/road element, house, корпус, строение, room, and FIAS ID -- Strength: official structured truth and building-level identifiers -- Integrated filter: retain active official Moscow/GKN records with valid FIAS - UUID, street, and house; deduplicate normalized simplified addresses; group - building identities before deterministic 90/5/5 splitting -- Result: 307,274 unique usable rows and 15,196 exact-value test rows -- Limitation: Moscow-only, clean legal formatting, and a stale 2021 snapshot -- Terms: the archive embeds Russian government open-data terms; the mirror - describes the package as CC-BY-SA. Confirm redistribution before publishing - derived rows. -- Status: integrated as a reproducible historical official-address benchmark - -## Evaluation policy - -Keep the three domains separate: - -1. historical bank-shaped regression data for compatibility; -2. RedMadRobot address windows for external noisy-input generalization; -3. Deepparse or official registries for broad clean-address coverage. - -Never average them into one headline accuracy number. Publish per-field metrics -and domain-specific slices, and reserve a new untouched dataset before changing -rules or features in response to observed external failures. - -## Local artifacts - -All raw and derived data stays under `.cache/external/` and is ignored by Git. -The runtime wheel contains none of it. The committed `deepparse_manifest.json` -and `datamos_manifest.json` record exact filters, counts, revisions, checksums, -split policies, and limitations; the matching report JSON files record the -first untuned scores. diff --git a/evaluation/FAILURE_ANALYSIS.md b/evaluation/FAILURE_ANALYSIS.md deleted file mode 100644 index d1ac0ec..0000000 --- a/evaluation/FAILURE_ANALYSIS.md +++ /dev/null @@ -1,102 +0,0 @@ -# Failure analysis - -This note reviews the current 500-row historical parsing regression and the -complete-message detection diagnostic. It separates parser behavior, ambiguous -input, and questionable reference expectations instead of treating every -mismatch as the same kind of model error. - -## Historical parsing regression - -Current result: - -- 500 rows; -- 402 exact rows and 98 non-exact rows; -- 80.4% exact-address rate; -- 95.8538% exact component-value micro F1; -- 76.8% of rows with no unparsed word or number span. - -`legacy_reference_500_diagnostics.csv` contains more than 75 columns for every -row. `legacy_reference_500_failure_summary.json` contains the complete primary -and secondary cause counts. - -The primary heuristic cause partitions the 98 non-exact rows: - -| Primary likely cause | Rows | -| --- | ---: | -| Conflicting street markers | 22 | -| Reference infers a street type absent from raw text | 20 | -| Ambiguous or unsupported abbreviation | 15 | -| Unmarked numeric-role ambiguity | 14 | -| Compound/letter-number boundary | 7 | -| Administrative label or boundary | 6 | -| Street-type recognition | 4 | -| Reference conflicts with explicit numeric marker | 3 | -| Numeric component not recognized | 3 | -| Four single-row causes | 4 | - -The primary assignment is a deterministic triage aid. Some rows have secondary -causes, and only a reviewer with authority over the reference schema can -adjudicate whether the parser or expected value should change. - -## Ten representative failures - -These ten were checked against the raw string, expected fields, current output, -and unparsed evidence. This is a behavior review, not verification against an -authoritative address registry. - -| ID | Narrow scenario | Observed mismatch | Reason | -| --- | --- | --- | --- | -| `legacy-good-0005` | `ул.Рязанский проспект` | Expected `пр-кт`, actual `ул` | Two explicit street markers compete. The parser selects the first and leaves `проспект` unparsed. This needs a marker-precedence or ambiguity policy. | -| `legacy-good-0359` | `Батайский проезд, 17, 294` | House becomes `294`; apartment missing | Both numeric roles are unmarked. Rightmost-tail logic selects the last number as house, leaving `17` unparsed. The input cannot be resolved safely without a numeric-role convention or alternative. | -| `legacy-good-0165` | `пр.Ленинского Комсомола` | Expected `пр-кт`, actual type missing | `пр.` is ambiguous between `проспект` and `проезд`; the current grammar does not silently choose. The expected value makes a choice not recoverable from the abbreviation alone. | -| `legacy-good-0751` | `ул.Ореховый бул.` | Street includes `Бул`; type is `ул` instead of `б-р` | Conflicting markers cause first-marker selection and a street boundary error. Both the type decision and value boundary need review. | -| `legacy-good-0089` | `Михайлово-Ярцевское п, Исаково д` | District missing; settlement receives district value | Suffix one-letter administrative markers collide with the parser's prefix-oriented marker grammar, shifting the component label and leaving `Исаково д` unparsed. | -| `legacy-good-0795` | `Ордынка Б., ... с.1` | Punctuation differs; type and structure missing | The reference supplies an implicit `ул`, while `с.` is an ambiguous unsupported short form. The remaining street difference is punctuation-only. This row mixes reference inference, abbreviation policy, and normalization. | -| `legacy-good-0006` | `г. Москва Денисовский переулок` | City absorbs street text; street becomes `Переулок` | A missing separator between city and street causes boundary merging before the suffix street marker is interpreted. | -| `legacy-good-0935` | `Котляковская, 4` | Expected `ул`, actual type missing | No street-type marker exists in the raw string. Scoring an inferred `ул` as an extraction error is a reference-policy issue and should be reviewed before changing the parser. | -| `legacy-good-0029` | `ул. Одоевского` | Expected `пр-д`, actual `ул` | The raw text explicitly says `ул`, while the reference expects `проезд`. The parser follows the source; this is a direct source/reference conflict. | -| `legacy-good-0036` | `стр. 4А` | Expected house `4А`; actual structure `4А` | The raw marker explicitly says `строение`, while the reference places the value in `house_num`. The parser follows the marker; the expected schema needs adjudication. | - -At least 24 rows should receive reference review before parser optimization: -20 inferred-but-absent street types, one explicit street-type conflict, and -three explicit numeric-marker conflicts. Fixing the parser to match these rows -without adjudication would increase the score while making source-faithful -extraction worse. - -## Complete-message address detection - -`evaluate_redmadrobot_detection.py` reconstructs all 2,841 source messages and -defines a gold address window only when a location cluster contains both -`STREET` and `HOUSE`. There are 144 such gold spans in 135 messages; the other -2,706 messages are annotated negatives for this narrow detection definition. - -Current development result: - -| Metric | Result | -| --- | ---: | -| Any-overlap precision | 98.0% | -| Any-overlap recall | 68.1% | -| Any-overlap F1 | 80.3% | -| Exact-boundary F1 | 23.8% | -| Negative-message specificity | 100.0% | - -The 107 non-exact messages carry explicit overlapping reason tags: - -| Detection failure tag | Messages | -| --- | ---: | -| Missed address | 44 | -| Span includes context outside gold | 33 | -| Span drops gold text | 33 | -| Spurious address | 1 | - -The main missed-address scenarios are markerless or transliterated addresses, -reversed component order, unusual abbreviations, and gold spans without the -strong street/building evidence required by the conservative detector. Boundary -differences commonly involve optional city, postcode, country, company/person -text, or trailing unit fields. - -The RedMadRobot detector failures were inspected while developing the current -algorithm. This report is therefore a development diagnostic and must not be -described as untouched final-test performance. A replacement final set needs -complete representative messages, hard negatives, grouped entities, and an -explicit boundary policy. diff --git a/evaluation/README.md b/evaluation/README.md deleted file mode 100644 index 7acf53d..0000000 --- a/evaluation/README.md +++ /dev/null @@ -1,368 +0,0 @@ -# Evaluation - -This directory measures two separate tasks: - -1. **address parsing** after an address string or oracle-cropped window is - already available; -2. **address detection** of half-open address spans inside a free-form message. - -Do not use parsing scores as evidence that the package can find addresses in -arbitrary prose. Do not use the small detection fixture as a production -accuracy claim. - -[`RESULTS.md`](RESULTS.md) indexes every committed JSON/CSV result artifact and -the command that reproduces it. - -## What parsing accuracy currently means - -The historical regression requires case-insensitive exact component values -after whitespace and `ё/е` folding. It reports: - -- exact-address rate: every public component value matches on one row; -- no-unparsed rate: no residual word or number spans remain; -- exact component-value micro precision, recall, and F1; -- the same exact-value metrics per public field. - -RedMadRobot instead uses one-to-one same-label span overlap on gold-cropped -address windows. Deepparse reports binary span overlap, character overlap, -token labels, and exact complete sequences. Moscow uses exact component values. -These metrics and domains are intentionally not interchangeable. - -## Historical 500-row regression - -`legacy_reference_500.jsonl` is a deterministic SHA-256 selection of 500 unique -rows from the `Good` worksheet in `ref/references.xlsx`. A row is eligible only -when: - -- the street and house are present; -- supported administrative fields appear as exact token sequences in the source - address; -- house, корпус/строение, and apartment values fit the parser's numeric schema; -- the legacy hierarchy can be represented by the v2 API. - -This filtering prevents corrected FIAS values from being scored as if the -parse-only package were expected to correct or resolve them. - -The workbook was already published in the historical repository as a reference -sample. The selected rows have **not** been independently re-reviewed during the -v2 work and are not bundled in the wheel. A canonical-grouped subset now trains -the compact tagger, while disjoint groups are reserved for validation and test. -Describe the data as a legacy reference or silver corpus—not a new gold dataset. - -Run the alpha regression gate: - -```bash -python evaluation/evaluate.py \ - --data evaluation/legacy_reference_500.jsonl \ - --gates evaluation/release_gates.json \ - --output evaluation/legacy_reference_500_report.json -``` - -This is the portable release gate: it uses only the committed 500-row fixture, -requires no download, and runs as part of the normal test suite. The report -names its exact-value aggregate `exact_component_value_micro`; the legacy -`micro` key remains as a compatibility alias for existing gate files. -The complete generated result is committed as -[`legacy_reference_500_report.json`](legacy_reference_500_report.json). - -The `2.0.0a1` baseline is: - -- 500 rows; -- 80.4% exact-address match; -- 95.9% micro field F1; -- 76.8% with no residual word or number tokens. - -The evaluator reports precision, recall, and F1 for every public field and keeps -a bounded failure sample. Gate thresholds are intentionally just below the -measured deterministic baseline: they prevent regressions but do not establish -production accuracy. - -### Row-level failure diagnostics - -Generate the complete 500-row diagnostic table and summary: - -```bash -python evaluation/analyze_failures.py -``` - -[`legacy_reference_500_diagnostics.csv`](legacy_reference_500_diagnostics.csv) -contains one row per test example and explicit columns for: - -- expected, actual, and match/missing/extra/wrong status for every field; -- mismatch, missing, extra, and wrong-value field lists; -- parser confidence, warnings, alternatives, and unparsed spans; -- punctuation, Unicode whitespace, marker position, administrative, unit, - compound-number, numeric-sequence, ordinal, and repeated-city scenarios; -- triage priority, failure types, likely causes, and a readable summary. - -The likely-cause fields are deterministic hypotheses for triage. They have not -been independently human-verified and must not be presented as causal ground -truth. The current summary contains 402 exact rows and 98 non-exact rows. -Among those failures, 61 involve `street_type`, 25 `house_num`, 21 `apartment`, -and 20 `street`; one row can contribute to several counts. - -The primary heuristic cause partitions all 98 rows: - -| Primary likely cause | Rows | Interpretation | -| --- | ---: | --- | -| Conflicting street markers | 22 | More than one type marker competes | -| Reference infers absent street type | 20 | Expected type is not explicit in raw text | -| Ambiguous/unsupported abbreviation | 15 | `пр.`, `с.`, `ком.`, or a typo needs review | -| Unmarked numeric-role ambiguity | 14 | Bare numbers can be house/corpus/unit | -| Compound or letter-number boundary | 7 | Slash, hyphen, or letter suffix is split | -| Administrative label/boundary | 6 | Adjacent administrative values merge or shift | -| Street-type recognition | 4 | A visible supported-looking marker is missed | -| Reference conflicts with numeric marker | 3 | Raw `стр.` conflicts with expected `house_num` | -| Numeric component not recognized | 3 | House/unit evidence is missed | -| Four single-row causes | 4 | Label confusion, reference conflict, or span/extra field | - -Additional likely-cause tags intentionally overlap—for example, an unmarked -numeric row can also contain label confusion and a missing apartment. Both the -primary partition and all secondary tags are retained in the CSV. - -[`FAILURE_ANALYSIS.md`](FAILURE_ANALYSIS.md) walks through ten representative -rows and explains why at least 24 failures require reference adjudication before -parser optimization. - -## Free-form message detection - -`detection_reference.jsonl` contains 30 deliberately narrow positive and -negative messages. Every row records the message, exact expected substrings, -scenario family, context style, address style, boundary style, polarity, -ambiguity, and notes. - -Run: - -```bash -python evaluation/evaluate_detection.py -``` - -The current conservative detector exactly matches all 20 annotated address -spans and returns no span for all 12 negative messages. That is 100% on this -small authored regression fixture only. It is not independent or large enough -for an accuracy claim. The next meaningful detector benchmark should annotate -complete, representative messages—including hard negatives—without -oracle-cropping. - -An additional diagnostic runs the detector on all 2,841 complete reconstructed -RedMadRobot messages rather than gold-cropped snippets: - -```bash -python evaluation/evaluate_redmadrobot_detection.py \ - --data .cache/external/redmadrobot-pii-benchmark-f77ea831.csv -``` - -Only gold clusters containing both `STREET` and `HOUSE` are address positives. -The current development snapshot has 144 such gold spans in 135 messages: - -| Detection metric | Result | -| --- | ---: | -| Any-overlap precision | 98.0% | -| Any-overlap recall | 68.1% | -| Any-overlap F1 | 80.3% | -| Exact-boundary F1 | 23.8% | -| Negative-message specificity | 100.0% | - -Exact-boundary scoring is much lower because the BIO gold span and detector -have different boundary policies—for example, one may include a city or -country while the other returns the parseable street/building/unit substring. -The complete diagnostic contains 107 non-exact messages: 44 missed-address, -33 context-inclusion, 33 dropped-gold-text, and one spurious-address tag. -Tags overlap. - -These failures were inspected while developing the detector, so this -RedMadRobot detection report is now a development diagnostic, not a sealed -final test. A production claim needs a new untouched message-level benchmark -whose annotation policy explicitly defines optional city, postcode, country, -person-name, and trailing-unit boundaries. - -## Independent external benchmark - -The repository also includes an adapter for -[RedMadRobot's MIT-licensed Russian PII NER benchmark](https://huggingface.co/datasets/redmadrobot-rnd/pii_benchmark). -Its 2,841 manually BIO-annotated sentences combine -production-log-shaped inputs (with real personal values replaced), synthetic -document-style examples, and manually filtered hard negatives. The external -data is pinned by Git revision and SHA-256 but is not committed or used for -training. - -Run the external evaluation once with: - -```bash -python evaluation/evaluate_redmadrobot.py --download \ - --output evaluation/redmadrobot_report.json -``` - -Subsequent runs can omit `--download`. The adapter extracts minimal address -windows from the gold BIO annotations and scores one-to-one, same-label span -overlap for `REGION`, `DISTRICT`, `CITY`, `STREET`, and `HOUSE`. This measures -address parsing after an address window has already been identified; it is not -an address-in-arbitrary-text detection score. `COUNTRY` is retained as context -but is not scored because it is not currently a public parser field. - -Treat this set as sealed evaluation data: do not train on it, tune thresholds -against it, or turn its failures into model features without replacing it with a -new untouched final test. - -The first untuned baseline covers 1,010 address spans in 578 snippets from 493 -source rows: - -| Slice | Snippets | Micro span F1 | -| --- | ---: | ---: | -| All address windows | 578 | 58.7% | -| Two or more distinct fields | 217 | 73.6% | -| Contains both street and house | 144 | 78.8% | -| Administrative fields only | 403 | 41.7% | - -Per-field F1 on all windows is 52.4% region, 44.5% district, 59.8% city, 49.1% -street, and 90.2% house. The large difference from the legacy regression is -important evidence: the parser is useful for conventional street-and-house -inputs, but it currently defaults too readily to `STREET` on isolated -administrative names and has weak administrative recall. - -## Large external corpora - -Install the data-only tools in a separate environment: - -```bash -python -m venv .venv-evaluation -.venv-evaluation/bin/python -m pip install -r requirements-evaluation.txt -``` - -The runtime wheel remains dependency-free. Raw and derived files are written to -the ignored `.cache/external/` directory. - -### Nationwide clean addresses: Deepparse - -Prepare the complete pinned Russian shard: - -```bash -.venv-evaluation/bin/python evaluation/prepare_deepparse.py \ - --download --overwrite -``` - -The streaming pipeline verifies the 371,595,309-byte source by SHA-256, checks -all 13,152,918 token/tag sequences, filters administrative-only strings, -deduplicates normalized text, maps the external tags to package fields, and -assigns canonical building groups to deterministic 90/5/5 splits. The result is: - -- 6,314,158 unique usable rows; -- 5,681,842 train, 316,586 validation, and 315,730 test rows; -- 5,293,689 street-and-house rows, including 679,076 with a unit; -- a 557,321,339-byte labeled Parquet corpus; -- a deterministic 100,000-row compressed JSONL sample from test only. - -Run the large test: - -```bash -python evaluation/evaluate_deepparse.py \ - --output evaluation/deepparse_report.json -``` - -The report uses explicit metric-family keys: - -- `span_overlap_micro`: binary same-label span matching with any overlap; -- `character_overlap_micro`: overlapping-character precision, recall, and F1; -- `token_label_micro`: aligned source-token label precision, recall, and F1; -- `exact_address_rate`: exact labels and exact span boundaries for a full row; -- `exact_token_sequence_rate`: exact complete token-label sequence. - -The original `micro`, `character_micro`, and `token_micro` names remain -compatibility aliases. Each generated report includes `metric_definitions`; -do not compare or average values from different metric families. - -The initial untuned 100,000-row result is: - -| Measure | Result | -| --- | ---: | -| Binary same-label span-overlap F1 | 84.4% | -| Character-overlap F1 | 66.2% | -| Token-label F1 | 66.5% | -| Exact token-boundary sequence | 8.0% | -| Throughput | 7,245 rows/s | - -Binary span overlap is intentionally lenient: any overlapping same-label span is -a match. Character and token metrics expose partial values and merged spans, but -also penalize schema-boundary differences such as the source labeling `дом 12` -as one entity while the package returns the value `12`. Publish all three, not -only the largest number. - -Character-overlap F1 by field is 99.9% postcode, 22.3% region, 22.6% district, -52.2% city, 76.7% street, 59.0% house, and 47.1% apartment. The set is national -in scale but consists of curated open-geographic addresses rather than noisy -user input. - -### Official clean addresses: Moscow registry - -Prepare the pinned October 2021 city snapshot: - -```bash -.venv-evaluation/bin/python evaluation/prepare_datamos.py \ - --download --overwrite -``` - -The filter retains only addresses that are on Moscow territory, official, -registered in the address registry, present in GKN, have a valid FIAS UUID, and -contain structured street and house values. It removes normalized duplicates -and groups street/house/корпус/строение identities before splitting. - -The result contains 307,274 unique active official addresses: 276,368 train, -15,710 validation, and 15,196 test. The portable filtered artifact is -26,050,795 bytes compressed. - -Run exact-value evaluation: - -```bash -python evaluation/evaluate_datamos.py \ - --output evaluation/datamos_report.json -``` - -The exact-value aggregate is named `exact_component_value_micro`; `micro` -remains a compatibility alias for the first published report schema. - -| Measure | Result | -| --- | ---: | -| Exact component-value micro F1 | 85.4% | -| Exact full-address match | 64.9% | -| Street F1 | 66.2% | -| House F1 | 98.2% | -| Корпус F1 | 99.6% | -| Строение F1 | 97.0% | - -This is the strongest current clean-building benchmark because it scores exact -structured values rather than mere span overlap. It is still not a production -claim: the snapshot is old, Moscow-only, and legally formatted. - -The archive embeds the original portal dataset ID, publisher, version, source -URL, and Russian government open-data terms. The mirror describes the package -as CC-BY-SA. Keep both records and confirm redistribution terms before -publishing derived rows. - -## Current evidence, kept separate - -| Domain | Test size | Primary measure | Baseline | -| --- | ---: | --- | ---: | -| Historical bank-shaped reference | 500 | exact field micro F1 | 95.9% | -| RedMadRobot noisy address windows | 578 | binary span-overlap F1 | 58.7% | -| Deepparse nationwide clean strings | 100,000 | character-overlap F1 | 66.2% | -| Moscow official clean buildings | 15,196 | exact component-value F1 | 85.4% | - -These numbers answer different questions and must not be averaged into one -“accuracy” claim. - -## Promoting this to a gold benchmark - -Before making a production-quality claim: - -1. confirm that the legacy workbook may be retained and used for evaluation; -2. have a person review at least 300 rows against the raw text and v2 schema; -3. record reviewer, decision, notes, and review date; -4. exclude corrected registry values that do not occur in the raw input; -5. group variations of one canonical address into the same data split; -6. keep a final test split that is never used to tune rules or the model; -7. publish field metrics, confidence intervals, slice failures, and limitations; -8. do not distribute address rows unless their provenance permits it. - -The existing reference is valuable enough to drive engineering now, but the -remaining human review is a release-management task, not something automation -should silently pretend to have completed. diff --git a/evaluation/RESULTS.md b/evaluation/RESULTS.md deleted file mode 100644 index 5f87209..0000000 --- a/evaluation/RESULTS.md +++ /dev/null @@ -1,91 +0,0 @@ -# Committed evaluation results - -This file is the index of durable benchmark results stored in the repository. -It separates message detection, address parsing, learned-model evaluation, and -software tests because they answer different questions. - -## Result snapshot - -| Task and domain | Rows or examples | Primary result | Committed report | -| --- | ---: | ---: | --- | -| Historical component parsing | 500 addresses | 95.85% exact component-value micro F1; 80.4% exact full address | [`legacy_reference_500_report.json`](legacy_reference_500_report.json) | -| Historical failure analysis | 500 addresses, 98 non-exact | Complete row-level diagnoses and scenario flags | [`legacy_reference_500_diagnostics.csv`](legacy_reference_500_diagnostics.csv), [`legacy_reference_500_failure_summary.json`](legacy_reference_500_failure_summary.json) | -| Compact learned tagger | 70 group-disjoint sequence examples | 96.52% token accuracy; 96.88% micro entity F1 | [`../training/model_evaluation.json`](../training/model_evaluation.json) | -| Authored message detection fixture | 30 messages, 20 spans | 100% exact span F1; regression fixture only | [`detection_report.json`](detection_report.json) | -| Complete-message detection diagnostic | 2,841 messages, 144 gold spans | 98.0% overlap precision; 68.1% recall; 80.3% F1 | [`redmadrobot_detection_report.json`](redmadrobot_detection_report.json) | -| RedMadRobot oracle-cropped parsing | 578 address windows | 58.75% same-label span-overlap F1 | [`redmadrobot_report.json`](redmadrobot_report.json) | -| Deepparse nationwide clean parsing | 100,000 addresses | 84.39% binary span-overlap F1; 66.23% character-overlap F1 | [`deepparse_report.json`](deepparse_report.json) | -| Official Moscow clean-building parsing | 15,196 addresses | 85.36% exact component-value F1; 64.93% exact full address | [`datamos_report.json`](datamos_report.json) | - -These numbers are not interchangeable and must not be averaged. Open each -report's `scope`, `limitations`, and `metric_definitions` before using a result. - -## Source fixtures and manifests - -The reports are accompanied by the exact portable inputs or source metadata -needed to interpret or reproduce them: - -| Artifact | Purpose | -| --- | --- | -| [`legacy_reference_500.jsonl`](legacy_reference_500.jsonl) | Portable historical parser fixture | -| [`release_gates.json`](release_gates.json) | Minimum non-regression thresholds for that fixture | -| [`detection_reference.jsonl`](detection_reference.jsonl) | Narrow positive and negative detector scenarios | -| [`deepparse_manifest.json`](deepparse_manifest.json) | Pinned source, preparation, and split metadata | -| [`datamos_manifest.json`](datamos_manifest.json) | Pinned Moscow source, filtering, and split metadata | -| [`DATA_SOURCES.md`](DATA_SOURCES.md) | Provenance and licensing notes | - -Large external raw/test corpora are intentionally not committed. Their reports -contain pinned source revisions and checksums; preparation writes external data -under the ignored `.cache/external/` directory. - -## Reproduction commands - -Historical parsing and gates: - -```bash -python evaluation/evaluate.py \ - --data evaluation/legacy_reference_500.jsonl \ - --gates evaluation/release_gates.json \ - --output evaluation/legacy_reference_500_report.json -``` - -Historical failure diagnostics: - -```bash -python evaluation/analyze_failures.py -``` - -Learned tagger: - -```bash -python training/train_compact_tagger.py -python training/evaluate_compact_tagger.py -``` - -Detection: - -```bash -python evaluation/evaluate_detection.py -python evaluation/evaluate_redmadrobot_detection.py \ - --data .cache/external/redmadrobot-pii-benchmark-f77ea831.csv -``` - -External parsing: - -```bash -python evaluation/evaluate_redmadrobot.py \ - --data .cache/external/redmadrobot-pii-benchmark-f77ea831.csv -python evaluation/evaluate_deepparse.py -python evaluation/evaluate_datamos.py -``` - -See [`README.md`](README.md) for the exact data preparation commands and -benchmark boundaries. - -## Software tests - -`pytest` verifies API, offsets, behavior, evaluation adapters, failure -diagnostics, artifact loading, and regression gates. A passing test count is an -execution/CI result rather than a model-quality metric, so transient console -logs are not committed as benchmark evidence. The benchmark JSON/CSV artifacts -above are the durable results. diff --git a/evaluation/analyze_failures.py b/evaluation/analyze_failures.py deleted file mode 100644 index 1030b7d..0000000 --- a/evaluation/analyze_failures.py +++ /dev/null @@ -1,603 +0,0 @@ -#!/usr/bin/env python3 -"""Create a row-level diagnostic table for the historical reference set. - -The likely-cause labels are deterministic triage hints, not human-verified -causal ground truth. They make recurring failure shapes visible before a -maintainer decides whether the parser, the reference label, or both need work. -""" - -from __future__ import annotations - -import argparse -from collections import Counter -import csv -import json -from pathlib import Path -import re -import sys -from typing import Any, Iterable - - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT / "src")) - -from address_normalizer import parse -from address_normalizer.types import ParsedAddress - - -FIELDS = ( - "postal_code", - "region", - "district", - "city", - "settlement", - "street", - "street_type", - "house_num", - "corpus", - "structure", - "apartment", -) -NUMERIC_FIELDS = {"house_num", "corpus", "structure", "apartment"} -ADMIN_FIELDS = {"region", "district", "city", "settlement"} -STREET_TYPE_PATTERNS = { - "ул": re.compile(r"(? str | None: - if value is None: - return None - return " ".join(value.casefold().replace("ё", "е").split()) - - -def _boundary_fold(value: str | None) -> str | None: - folded = _fold(value) - if folded is None: - return None - return re.sub(r"[\W_]+", "", folded) - - -def _load_rows(path: Path) -> list[dict[str, Any]]: - rows: list[dict[str, Any]] = [] - for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): - if not line.strip(): - continue - row = json.loads(line) - if not isinstance(row, dict) or not isinstance(row.get("raw"), str): - raise ValueError(f"{path}:{line_number}: invalid evaluation row") - if not isinstance(row.get("expected"), dict): - raise ValueError(f"{path}:{line_number}: expected must be an object") - rows.append(row) - return rows - - -def _values(result: ParsedAddress) -> dict[str, str | None]: - return { - field: ( - getattr(result, field).value - if getattr(result, field) is not None - else None - ) - for field in FIELDS - } - - -def _field_status(expected: str | None, actual: str | None) -> str: - wanted = _fold(expected) - observed = _fold(actual) - if wanted == observed: - return "match" - if wanted is not None and observed is None: - return "missing" - if wanted is None and observed is not None: - return "extra" - return "wrong_value" - - -def _street_marker_style(raw: str, expected_street: str | None) -> str: - markers = list(ANY_STREET_MARKER_RE.finditer(raw)) - if not markers: - return "absent" - if len(markers) > 1: - return "multiple" - if not expected_street: - return "present_unknown_position" - street_index = _fold(raw).find(_fold(expected_street) or "") - if street_index < 0: - return "present_street_not_literal" - return "prefix" if markers[0].start() <= street_index else "suffix" - - -def _scenario_columns( - raw: str, - expected: dict[str, Any], -) -> dict[str, str]: - expected_street = ( - str(expected["street"]) if expected.get("street") is not None else None - ) - expected_city = ( - str(expected["city"]) if expected.get("city") is not None else None - ) - marker_style = _street_marker_style(raw, expected_street) - booleans = { - "has_postal_code": bool(POSTAL_RE.search(raw)), - "has_country_phrase": bool(COUNTRY_RE.search(raw)), - "has_administrative_expected": any( - expected.get(field) is not None for field in ADMIN_FIELDS - ), - "has_unit_expected": any( - expected.get(field) is not None - for field in ("corpus", "structure", "apartment") - ), - "has_street_marker": bool(ANY_STREET_MARKER_RE.search(raw)), - "has_house_marker": bool(HOUSE_MARKER_RE.search(raw)), - "has_unit_marker": bool(UNIT_MARKER_RE.search(raw)), - "has_compact_punctuation": bool(COMPACT_PUNCTUATION_RE.search(raw)), - "has_unicode_whitespace": any( - character.isspace() and character != " " for character in raw - ), - "has_compound_number": bool(COMPOUND_NUMBER_RE.search(raw)), - "has_slash_number": bool(re.search(r"\d\s*/\s*\d", raw)), - "has_hyphenated_number": bool(re.search(r"\d\s*-\s*\d", raw)), - "has_letter_suffix_number": bool(LETTER_SUFFIX_RE.search(raw)), - "has_unmarked_numeric_sequence": bool( - UNMARKED_NUMERIC_SEQUENCE_RE.search(raw) - ), - "has_ordinal_street": bool(ORDINAL_STREET_RE.search(raw)), - "has_ambiguous_abbreviation": bool( - AMBIGUOUS_ABBREVIATION_RE.search(raw) - ), - "has_multiword_street": bool( - expected_street and len(expected_street.split()) > 1 - ), - "has_repeated_city": bool( - expected_city - and _fold(raw).count(_fold(expected_city) or "") > 1 - ), - } - tags = [ - name.removeprefix("has_") - for name, present in booleans.items() - if present - ] - tags.append(f"street_marker_{marker_style}") - return { - **{name: str(value).lower() for name, value in booleans.items()}, - "street_marker_style": marker_style, - "scenario_tags": "|".join(tags), - } - - -def _expected_street_marker_is_visible( - raw: str, - expected_type: str | None, -) -> bool: - if expected_type is None: - return False - pattern = STREET_TYPE_PATTERNS.get(expected_type) - return bool(pattern and pattern.search(raw)) - - -def _diagnose( - raw: str, - expected: dict[str, str | None], - actual: dict[str, str | None], - statuses: dict[str, str], - result: ParsedAddress, -) -> tuple[list[str], list[str], str]: - mismatch_fields = [ - field for field, status in statuses.items() if status != "match" - ] - failure_types = sorted({statuses[field] for field in mismatch_fields}) - causes: set[str] = set() - - expected_by_value = { - _fold(value): field - for field, value in expected.items() - if value is not None - } - for field in mismatch_fields: - wanted = expected.get(field) - observed = actual.get(field) - wanted_folded = _fold(wanted) - observed_folded = _fold(observed) - - if observed_folded is not None and observed_folded in expected_by_value: - if expected_by_value[observed_folded] != field: - causes.add("component_label_confusion") - - if field == "street_type": - if AMBIGUOUS_ABBREVIATION_RE.search(raw): - causes.add("ambiguous_or_unsupported_abbreviation") - elif ( - wanted is not None - and not _expected_street_marker_is_visible(raw, wanted) - ): - if ANY_STREET_MARKER_RE.search(raw): - causes.add("reference_conflicts_with_explicit_street_type") - else: - causes.add("reference_infers_missing_street_type") - elif len(list(ANY_STREET_MARKER_RE.finditer(raw))) > 1: - causes.add("conflicting_street_markers") - else: - causes.add("street_type_recognition") - continue - - if field in NUMERIC_FIELDS: - if AMBIGUOUS_ABBREVIATION_RE.search(raw): - causes.add("ambiguous_or_unsupported_abbreviation") - if UNMARKED_NUMERIC_SEQUENCE_RE.search(raw): - causes.add("unmarked_numeric_role_ambiguity") - if wanted and observed and ( - wanted_folded in (observed_folded or "") - or (observed_folded or "") in (wanted_folded or "") - ): - causes.add("compound_or_letter_number_boundary") - elif wanted and ( - "/" in wanted or "-" in wanted or LETTER_SUFFIX_RE.search(wanted) - ): - causes.add("compound_or_letter_number_boundary") - elif statuses[field] == "missing": - causes.add("numeric_component_not_recognized") - elif statuses[field] == "extra": - causes.add("spurious_numeric_component") - else: - causes.add("numeric_value_or_role") - continue - - if field in ADMIN_FIELDS: - if statuses[field] == "missing": - causes.add("administrative_component_missed") - elif statuses[field] == "extra": - causes.add("spurious_administrative_component") - else: - causes.add("administrative_label_or_boundary") - continue - - if field == "street": - if _boundary_fold(wanted) == _boundary_fold(observed): - causes.add("normalization_only_difference") - elif wanted_folded and observed_folded and ( - wanted_folded in observed_folded - or observed_folded in wanted_folded - ): - causes.add("street_span_boundary") - elif statuses[field] == "missing": - causes.add("street_not_recognized") - else: - causes.add("street_label_or_value") - continue - - causes.add(f"{field}_{statuses[field]}") - - for expected_field in NUMERIC_FIELDS: - wanted = _fold(expected.get(expected_field)) - if wanted is None: - continue - for actual_field in NUMERIC_FIELDS - {expected_field}: - if ( - _fold(actual.get(actual_field)) == wanted - and expected.get(actual_field) is None - and NUMERIC_MARKER_PATTERNS[actual_field].search(raw) - ): - causes.add("reference_conflicts_with_explicit_numeric_marker") - summary = "; ".join( - f"{field}: expected={expected.get(field)!r}, actual={actual.get(field)!r}" - for field in mismatch_fields - ) - return failure_types, sorted(causes), summary - - -def _primary_cause(causes: Iterable[str]) -> str: - available = set(causes) - priority = ( - "reference_conflicts_with_explicit_numeric_marker", - "reference_conflicts_with_explicit_street_type", - "reference_infers_missing_street_type", - "ambiguous_or_unsupported_abbreviation", - "conflicting_street_markers", - "unmarked_numeric_role_ambiguity", - "compound_or_letter_number_boundary", - "component_label_confusion", - "numeric_component_not_recognized", - "numeric_value_or_role", - "administrative_component_missed", - "administrative_label_or_boundary", - "spurious_administrative_component", - "street_span_boundary", - "normalization_only_difference", - "street_type_recognition", - "street_label_or_value", - "street_not_recognized", - "spurious_numeric_component", - ) - return next( - (cause for cause in priority if cause in available), - sorted(available)[0] if available else "", - ) - - -def diagnose_row(row: dict[str, Any]) -> dict[str, str]: - """Return one flat, CSV-ready diagnostic record.""" - - raw = str(row["raw"]) - expected = { - field: ( - str(row["expected"][field]) - if row["expected"].get(field) is not None - else None - ) - for field in FIELDS - } - result = parse(raw) - actual = _values(result) - statuses = { - field: _field_status(expected[field], actual[field]) - for field in FIELDS - } - mismatch_fields = [ - field for field, status in statuses.items() if status != "match" - ] - missing_fields = [ - field for field, status in statuses.items() if status == "missing" - ] - extra_fields = [ - field for field, status in statuses.items() if status == "extra" - ] - wrong_value_fields = [ - field for field, status in statuses.items() if status == "wrong_value" - ] - failure_types, causes, failure_summary = _diagnose( - raw, - expected, - actual, - statuses, - result, - ) - if not mismatch_fields: - triage_priority = "none" - diagnosis_status = "not_applicable" - elif causes == ["reference_infers_missing_street_type"]: - triage_priority = "reference_review" - diagnosis_status = "heuristic_needs_human_review" - elif missing_fields or "component_label_confusion" in causes: - triage_priority = "high" - diagnosis_status = "heuristic_needs_human_review" - elif causes == ["normalization_only_difference"]: - triage_priority = "low" - diagnosis_status = "heuristic_needs_human_review" - else: - triage_priority = "medium" - diagnosis_status = "heuristic_needs_human_review" - - source = row.get("source") - source_row = source.get("row") if isinstance(source, dict) else None - output = { - "id": str(row.get("id", "")), - "dataset": "legacy_reference_500", - "source_row": "" if source_row is None else str(source_row), - "raw": raw, - "review_status": str(row.get("review_status", "unspecified")), - "exact_address": str(not mismatch_fields).lower(), - "triage_priority": triage_priority, - "diagnosis_status": diagnosis_status, - "parser_confidence": f"{result.confidence:.6f}", - "expected_component_count": str( - sum(value is not None for value in expected.values()) - ), - "actual_component_count": str( - sum(value is not None for value in actual.values()) - ), - "mismatch_count": str(len(mismatch_fields)), - "mismatch_fields": "|".join(mismatch_fields), - "missing_fields": "|".join(missing_fields), - "extra_fields": "|".join(extra_fields), - "wrong_value_fields": "|".join(wrong_value_fields), - "failure_types": "|".join(failure_types), - "primary_likely_cause": _primary_cause(causes), - "likely_causes": "|".join(causes), - "failure_summary": failure_summary, - "warnings": "|".join(result.warnings), - "unparsed_spans": json.dumps( - [part.raw for part in result.unparsed], - ensure_ascii=False, - separators=(",", ":"), - ), - "alternatives": json.dumps( - [alternative.as_dict() for alternative in result.alternatives], - ensure_ascii=False, - separators=(",", ":"), - ), - **_scenario_columns(raw, expected), - } - for field in FIELDS: - output[f"expected_{field}"] = expected[field] or "" - output[f"actual_{field}"] = actual[field] or "" - output[f"status_{field}"] = statuses[field] - return output - - -def _representative_failures( - diagnostics: Iterable[dict[str, str]], - limit: int = 10, -) -> list[dict[str, str]]: - selected: list[dict[str, str]] = [] - used_causes: set[str] = set() - failures = [ - row for row in diagnostics if row["exact_address"] == "false" - ] - for row in failures: - causes = row["likely_causes"].split("|") - if any(cause not in used_causes for cause in causes): - selected.append(row) - used_causes.update(causes) - if len(selected) == limit: - return selected - for row in failures: - if row not in selected: - selected.append(row) - if len(selected) == limit: - break - return selected - - -def summarize(diagnostics: list[dict[str, str]]) -> dict[str, Any]: - """Summarize failure counts without collapsing metric domains.""" - - failures = [ - row for row in diagnostics if row["exact_address"] == "false" - ] - cause_counts: Counter[str] = Counter() - primary_cause_counts: Counter[str] = Counter() - field_counts: Counter[str] = Counter() - scenario_counts: Counter[str] = Counter() - for row in failures: - cause_counts.update(filter(None, row["likely_causes"].split("|"))) - primary_cause_counts.update([row["primary_likely_cause"]]) - field_counts.update(filter(None, row["mismatch_fields"].split("|"))) - scenario_counts.update(filter(None, row["scenario_tags"].split("|"))) - sample_columns = ( - "id", - "raw", - "mismatch_fields", - "primary_likely_cause", - "likely_causes", - "failure_summary", - "unparsed_spans", - ) - return { - "dataset": "legacy_reference_500", - "rows": len(diagnostics), - "exact_rows": len(diagnostics) - len(failures), - "failed_rows": len(failures), - "diagnostic_semantics": ( - "likely_causes are deterministic triage hypotheses and require " - "human review; they are not causal ground truth" - ), - "failure_rows_by_likely_cause": dict(cause_counts.most_common()), - "failure_rows_by_primary_likely_cause": dict( - primary_cause_counts.most_common() - ), - "failure_rows_by_mismatch_field": dict(field_counts.most_common()), - "scenario_tags_on_failure_rows": dict(scenario_counts.most_common()), - "representative_failure_sample": [ - {column: row[column] for column in sample_columns} - for row in _representative_failures(diagnostics) - ], - } - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument( - "--data", - type=Path, - default=ROOT / "evaluation/legacy_reference_500.jsonl", - ) - parser.add_argument( - "--output", - type=Path, - default=ROOT / "evaluation/legacy_reference_500_diagnostics.csv", - ) - parser.add_argument( - "--summary-output", - type=Path, - default=ROOT / "evaluation/legacy_reference_500_failure_summary.json", - ) - args = parser.parse_args(argv) - - diagnostics = [diagnose_row(row) for row in _load_rows(args.data)] - args.output.parent.mkdir(parents=True, exist_ok=True) - with args.output.open("w", encoding="utf-8", newline="") as target: - writer = csv.DictWriter( - target, - fieldnames=list(diagnostics[0]), - lineterminator="\n", - ) - writer.writeheader() - writer.writerows(diagnostics) - - report = summarize(diagnostics) - rendered = json.dumps(report, ensure_ascii=False, indent=2) - args.summary_output.parent.mkdir(parents=True, exist_ok=True) - args.summary_output.write_text(f"{rendered}\n", encoding="utf-8") - print(rendered) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/evaluation/datamos_data.py b/evaluation/datamos_data.py deleted file mode 100644 index 22685d3..0000000 --- a/evaluation/datamos_data.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Pinned metadata and pure transformations for the Moscow address registry.""" - -from __future__ import annotations - -import hashlib -import json -from pathlib import Path -import re -from typing import Any -import uuid - - -ROOT = Path(__file__).resolve().parents[1] -DATASET_ID = 60_562 -DATASET_VERSION = "3.630" -DATASET_DATE = "2021-10-15" -SOURCE_ROWS = 440_399 -ARCHIVE_SHA256 = ( - "a272480189bf1e17e70b0e0b2e115520896ff1d168681d452697c3908ad75a66" -) -INNER_DATA_SHA256 = ( - "c253335e8b25fce0264837c026536689b33ff9fc5ac9d305158685d4528d95bb" -) -ARCHIVE_URL = ( - "https://data2.apicrafter.ru/packages/datamos-addressreestr/build/" - "datamos-7705031674-AddressReestr-2021-10-23-7-55/get" -) -DEFAULT_ARCHIVE = ( - ROOT / ".cache" / "external" / "datamos-addressreestr-2021-10-23.zip" -) -DEFAULT_FILTERED = ( - ROOT - / ".cache" - / "external" - / "datamos-addressreestr-usable-2021-10-23.jsonl.gz" -) -DEFAULT_MANIFEST = ROOT / "evaluation" / "datamos_manifest.json" -FIELDS = ("street", "house_num", "corpus", "structure") -_SPACE_RE = re.compile(r"\s+") - - -def fold(value: str) -> str: - return _SPACE_RE.sub(" ", value.casefold().replace("ё", "е")).strip(" ,.;") - - -def rejection_reason(row: dict[str, Any]) -> str | None: - if row.get("OnTerritoryOfMoscow") != "да": - return "outside_moscow" - if row.get("ADR_TYPE") != "Официальный": - return "not_official" - if row.get("SOSTAD") != "Зарегистрирован в АР": - return "not_registered" - if row.get("STATUS") != "Внесён в ГКН": - return "not_in_gkn" - for field in ("SIMPLE_ADDRESS", "P7", "L1_VALUE"): - if not str(row.get(field, "")).strip(): - return f"missing_{field.lower()}" - try: - uuid.UUID(str(row.get("N_FIAS", ""))) - except (ValueError, AttributeError): - return "invalid_fias_id" - return None - - -def expected_components(row: dict[str, Any]) -> dict[str, str | None]: - return { - "street": str(row["P7"]).strip(), - "house_num": str(row["L1_VALUE"]).strip(), - "corpus": str(row.get("L2_VALUE", "")).strip() or None, - "structure": str(row.get("L3_VALUE", "")).strip() or None, - } - - -def quality_tier(row: dict[str, Any]) -> str: - has_corpus = bool(str(row.get("L2_VALUE", "")).strip()) - has_structure = bool(str(row.get("L3_VALUE", "")).strip()) - if has_corpus and has_structure: - return "house_corpus_structure" - if has_corpus: - return "house_corpus" - if has_structure: - return "house_structure" - return "house_only" - - -def group_id_and_split(row: dict[str, Any]) -> tuple[str, str]: - identity = [ - fold(str(row.get(field, ""))) - for field in ("P7", "L1_VALUE", "L2_VALUE", "L3_VALUE") - ] - key = json.dumps( - identity, - ensure_ascii=False, - separators=(",", ":"), - ).encode("utf-8") - digest = hashlib.sha256(b"datamos-building-v1\0" + key).digest() - bucket = int.from_bytes(digest[:8], "big") % 10_000 - split = ( - "train" - if bucket < 9_000 - else "validation" - if bucket < 9_500 - else "test" - ) - return digest[:16].hex(), split diff --git a/evaluation/datamos_manifest.json b/evaluation/datamos_manifest.json deleted file mode 100644 index 5f6c918..0000000 --- a/evaluation/datamos_manifest.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "format_version": 1, - "source": { - "title": "Адресный реестр объектов недвижимости города Москвы", - "publisher": "Департамент городского имущества города Москвы", - "original_portal": "https://data.mos.ru", - "dataset_id": 60562, - "version": "3.630", - "release_date": "2021-10-15", - "mirror": "https://data2.apicrafter.ru/packages/datamos-addressreestr", - "archive_url": "https://data2.apicrafter.ru/packages/datamos-addressreestr/build/datamos-7705031674-AddressReestr-2021-10-23-7-55/get", - "archive_bytes": 121581813, - "archive_sha256": "a272480189bf1e17e70b0e0b2e115520896ff1d168681d452697c3908ad75a66", - "inner_data_sha256": "c253335e8b25fce0264837c026536689b33ff9fc5ac9d305158685d4528d95bb", - "source_rows": 440399, - "embedded_terms": "Типовые условия доступа к открытым данным органов власти в РФ", - "mirror_terms": "CC-BY-SA" - }, - "policy": { - "purpose": "Moscow-only official clean-address training/evaluation corpus; not bundled in the runtime package", - "filter": [ - "OnTerritoryOfMoscow == да", - "ADR_TYPE == Официальный", - "SOSTAD == Зарегистрирован в АР", - "STATUS == Внесён в ГКН", - "non-empty SIMPLE_ADDRESS, P7 street, and L1_VALUE house", - "valid N_FIAS UUID" - ], - "deduplication": "first case-folded, ё/е-folded, whitespace-normalized SIMPLE_ADDRESS", - "grouping": "street/house/corpus/structure identity; SHA-256 groups are assigned 90% train, 5% validation, 5% test" - }, - "counts": { - "duplicate_rows_removed": 893, - "object_Здание": 205838, - "object_Земельный участок": 97828, - "object_Сооружение": 3375, - "object_объект незавершенного строительства": 233, - "rejected_invalid_fias_id": 8245, - "rejected_missing_l1_value": 434, - "rejected_missing_p7": 84404, - "rejected_not_in_gkn": 23747, - "rejected_not_official": 14430, - "rejected_not_registered": 808, - "rejected_outside_moscow": 164, - "source_rows": 440399, - "split_test": 15196, - "split_train": 276368, - "split_validation": 15710, - "tier_house_corpus": 26072, - "tier_house_corpus_structure": 8178, - "tier_house_only": 158680, - "tier_house_structure": 114344, - "unique_usable_rows": 307274 - }, - "artifact": { - "filename": "datamos-addressreestr-usable-2021-10-23.jsonl.gz", - "rows": 307274, - "bytes": 26050795, - "sha256": "bba7849a479f2169d2f438032c65e069badb76f3f19ee590041267de27f84c63" - }, - "limitations": [ - "The snapshot is from October 2021 and is not a current registry.", - "The corpus is Moscow-only and consists of clean legal formatting.", - "Only street, house, corpus, and structure are scored from SIMPLE_ADDRESS; administrative fields are intentionally out of scope for this view.", - "The archive is obtained from an attributed mirror; retain its embedded metadata and confirm redistribution terms before publishing derived rows." - ] -} diff --git a/evaluation/datamos_report.json b/evaluation/datamos_report.json deleted file mode 100644 index b656d93..0000000 --- a/evaluation/datamos_report.json +++ /dev/null @@ -1,776 +0,0 @@ -{ - "scope": "untuned exact-value evaluation on a group-disjoint test split of active official Moscow registry building addresses", - "source": { - "dataset_id": 60562, - "version": "3.630", - "release_date": "2021-10-15" - }, - "limitations": [ - "October 2021 snapshot; not current FIAS/GAR truth", - "Moscow-only clean legal/simplified address formatting", - "administrative fields and address existence resolution are unscored" - ], - "rows": 15196, - "tiers": { - "house_corpus": 1326, - "house_corpus_structure": 427, - "house_only": 7801, - "house_structure": 5642 - }, - "matching": "case-insensitive exact component value after whitespace and ё/е folding; street includes its source type marker", - "metric_definitions": { - "exact_component_value_micro": "micro precision, recall, and F1 over case-insensitive exact component values after whitespace and ё/е folding", - "exact_address_rate": "fraction of rows where every scored component value matches", - "no_unparsed_rate": "fraction of rows with no residual word or number spans", - "fields": "per-field exact component-value metrics" - }, - "exact_component_value_micro": { - "tp": 32604, - "fp": 5572, - "fn": 5610, - "support": 38214, - "precision": 0.854044, - "recall": 0.853195, - "f1": 0.85362 - }, - "micro": { - "tp": 32604, - "fp": 5572, - "fn": 5610, - "support": 38214, - "precision": 0.854044, - "recall": 0.853195, - "f1": 0.85362 - }, - "macro_field_f1": 0.902612, - "exact_address_rate": 0.649316, - "no_unparsed_rate": 0.399052, - "fields": { - "street": { - "tp": 10067, - "fp": 5129, - "fn": 5129, - "support": 15196, - "precision": 0.662477, - "recall": 0.662477, - "f1": 0.662477 - }, - "house_num": { - "tp": 14910, - "fp": 259, - "fn": 286, - "support": 15196, - "precision": 0.982926, - "recall": 0.981179, - "f1": 0.982052 - }, - "corpus": { - "tp": 1742, - "fp": 3, - "fn": 11, - "support": 1753, - "precision": 0.998281, - "recall": 0.993725, - "f1": 0.995998 - }, - "structure": { - "tp": 5885, - "fp": 181, - "fn": 184, - "support": 6069, - "precision": 0.970162, - "recall": 0.969682, - "f1": 0.969922 - } - }, - "elapsed_seconds": 4.568, - "rows_per_second": 3326.9, - "failure_sample": [ - { - "source_row": 1796, - "fias_id": "30f3b4ea-773f-467c-8e8c-6c31c6547d1e", - "tier": "house_corpus", - "raw": "шоссе Энтузиастов, дом 29, корпус 4-5", - "mismatches": { - "corpus": { - "expected": "4-5", - "actual": "4" - } - } - }, - { - "source_row": 2673, - "fias_id": "799d5735-0711-4568-a2fc-43dd9cac49f0", - "tier": "house_structure", - "raw": "улица Сергия Радонежского, дом 15-17, строение 1", - "mismatches": { - "house_num": { - "expected": "15-17", - "actual": "15" - } - } - }, - { - "source_row": 4460, - "fias_id": "689da5c9-fec1-4b99-859d-de4e3d96a0ce", - "tier": "house_corpus_structure", - "raw": "улица Ивана Франко, дом 48, корпус Г, строение 4", - "mismatches": { - "corpus": { - "expected": "Г", - "actual": null - } - } - }, - { - "source_row": 5693, - "fias_id": "167afc58-12ef-46c4-a920-45761736fea6", - "tier": "house_corpus", - "raw": "Нижняя Сыромятническая улица, дом 11, корпус Б", - "mismatches": { - "corpus": { - "expected": "Б", - "actual": null - } - } - }, - { - "source_row": 5697, - "fias_id": "e4b09246-6937-434f-93a7-9fcb2a6501ac", - "tier": "house_structure", - "raw": "проспект Вернадского, владение 6-В, строение 1", - "mismatches": { - "house_num": { - "expected": "6-В", - "actual": "6" - } - } - }, - { - "source_row": 6580, - "fias_id": "c08786fa-400b-43a1-a1ef-5e95f7ad08ab", - "tier": "house_structure", - "raw": "2-я улица Новосёлки, дом 11А, строение 1", - "mismatches": { - "street": { - "expected": "2-я улица Новосёлки", - "actual": "улица Новосёлки" - } - } - }, - { - "source_row": 7409, - "fias_id": "ac5f4612-0e9f-4341-9980-d801b4d35c0b", - "tier": "house_structure", - "raw": "3-й Лучевой просек, дом 12, строение 6", - "mismatches": { - "street": { - "expected": "3-й Лучевой просек", - "actual": "й Лучевой" - } - } - }, - { - "source_row": 7531, - "fias_id": "1e791410-8ef4-4ab4-ad26-b316d57c7899", - "tier": "house_only", - "raw": "3-я улица Ямского Поля, дом 15", - "mismatches": { - "street": { - "expected": "3-я улица Ямского Поля", - "actual": "улица Ямского Поля" - } - } - }, - { - "source_row": 7870, - "fias_id": "15b0092f-c9ca-430a-a8fd-e288489d6aa9", - "tier": "house_only", - "raw": "4-я улица Новосёлки, дом 11", - "mismatches": { - "street": { - "expected": "4-я улица Новосёлки", - "actual": "улица Новосёлки" - } - } - }, - { - "source_row": 8043, - "fias_id": "ce9ef716-fc4c-422b-9ac3-a0e61bc8f4b7", - "tier": "house_structure", - "raw": "1-й переулок Тружеников, дом 14, строение 10", - "mismatches": { - "street": { - "expected": "1-й переулок Тружеников", - "actual": "1-й переулок" - } - } - }, - { - "source_row": 9140, - "fias_id": "b404108e-a463-446f-9284-2b3f58e7dc79", - "tier": "house_structure", - "raw": "Большая Грузинская улица, дом 4-6, строение 9", - "mismatches": { - "house_num": { - "expected": "4-6", - "actual": "4" - } - } - }, - { - "source_row": 10350, - "fias_id": "5f99e9de-5042-4d63-a0a0-a303ee7ea32a", - "tier": "house_structure", - "raw": "2-я улица Новые Сады, дом 18, строение 4", - "mismatches": { - "street": { - "expected": "2-я улица Новые Сады", - "actual": "улица Новые Сады" - } - } - }, - { - "source_row": 10758, - "fias_id": "46be4353-2bd1-4790-ad0c-5874bf4434df", - "tier": "house_structure", - "raw": "Пулковская улица, дом 4, строение 9-10", - "mismatches": { - "structure": { - "expected": "9-10", - "actual": "9" - } - } - }, - { - "source_row": 12286, - "fias_id": "c42af633-132f-4fce-bb98-a084fedb7711", - "tier": "house_corpus", - "raw": "1-я Северная линия, дом 1, корпус 8", - "mismatches": { - "street": { - "expected": "1-я Северная линия", - "actual": "линия" - } - } - }, - { - "source_row": 12293, - "fias_id": "cdc8ff56-5542-4a0e-abf8-53ec1799a2a3", - "tier": "house_structure", - "raw": "1-я Северная линия, дом 1, строение 57", - "mismatches": { - "street": { - "expected": "1-я Северная линия", - "actual": "линия" - } - } - }, - { - "source_row": 12518, - "fias_id": "d54c749d-55d6-4ee4-8ad1-84ae055ff1c5", - "tier": "house_only", - "raw": "3-я Северная линия, дом 3", - "mismatches": { - "street": { - "expected": "3-я Северная линия", - "actual": "линия" - } - } - }, - { - "source_row": 12854, - "fias_id": "f183b356-3be9-489c-9229-08e0825f0467", - "tier": "house_only", - "raw": "8-я улица Текстильщиков, дом 8", - "mismatches": { - "street": { - "expected": "8-я улица Текстильщиков", - "actual": "улица Текстильщиков" - } - } - }, - { - "source_row": 13058, - "fias_id": "dc5c9b19-500b-4ace-8357-bb6f5db3c14d", - "tier": "house_only", - "raw": "8-я улица Новые Сады, дом 8", - "mismatches": { - "street": { - "expected": "8-я улица Новые Сады", - "actual": "улица Новые Сады" - } - } - }, - { - "source_row": 13807, - "fias_id": "b23db3e9-6909-4abb-8f29-08413bd256e0", - "tier": "house_only", - "raw": "улица Мичуринский Проспект, Олимпийская Деревня, дом 9", - "mismatches": { - "street": { - "expected": "улица Мичуринский Проспект, Олимпийская Деревня", - "actual": "улица Мичуринский" - } - } - }, - { - "source_row": 14627, - "fias_id": "35c43eca-cf6b-4eca-ac22-3d73189cf778", - "tier": "house_only", - "raw": "улица Большая Молчановка, дом 26-28", - "mismatches": { - "house_num": { - "expected": "26-28", - "actual": "26" - } - } - }, - { - "source_row": 15270, - "fias_id": "2ab4df2d-7409-48d2-bdd0-28182cc243e5", - "tier": "house_structure", - "raw": "2-й квартал Капотня, дом 11, строение 1", - "mismatches": { - "street": { - "expected": "2-й квартал Капотня", - "actual": "й квартал Капотня" - } - } - }, - { - "source_row": 15392, - "fias_id": "9b6a0ea4-4813-4c03-8d5c-bfe5303b7c2d", - "tier": "house_only", - "raw": "92-й километр Московской Кольцевой Автодороги, владение 3", - "mismatches": { - "street": { - "expected": "92-й километр Московской Кольцевой Автодороги", - "actual": "й" - } - } - }, - { - "source_row": 15527, - "fias_id": "1f123b98-cf51-4af4-bd63-6cee1b8f149e", - "tier": "house_only", - "raw": "7-я улица Текстильщиков, дом 3А", - "mismatches": { - "street": { - "expected": "7-я улица Текстильщиков", - "actual": "улица Текстильщиков" - } - } - }, - { - "source_row": 15824, - "fias_id": "1e0b6f18-33b8-4831-a6ed-910b5171e2e0", - "tier": "house_only", - "raw": "1-й переулок Тружеников, дом 13", - "mismatches": { - "street": { - "expected": "1-й переулок Тружеников", - "actual": "1-й переулок" - } - } - }, - { - "source_row": 16460, - "fias_id": "38ac0703-f4c3-42e8-8379-9c8ab0838ca2", - "tier": "house_structure", - "raw": "1-я Северная линия, дом 1, строение 25", - "mismatches": { - "street": { - "expected": "1-я Северная линия", - "actual": "линия" - } - } - }, - { - "source_row": 17035, - "fias_id": "33b42a72-eec5-4320-a76e-0e7f39b994eb", - "tier": "house_only", - "raw": "53-й километр Московской Кольцевой Автодороги, дом 6", - "mismatches": { - "street": { - "expected": "53-й километр Московской Кольцевой Автодороги", - "actual": "й" - } - } - }, - { - "source_row": 17599, - "fias_id": "1010ba97-452b-4e8f-954d-af0d30e355f6", - "tier": "house_structure", - "raw": "проектируемый проезд № 4062, дом 6, строение 13", - "mismatches": { - "street": { - "expected": "проектируемый проезд № 4062", - "actual": "проектируемый проезд" - } - } - }, - { - "source_row": 18632, - "fias_id": "63de66e8-93dd-43d8-8cec-4b5ecb70d6df", - "tier": "house_structure", - "raw": "2-я линия Хорошёвского Серебряного Бора, домовладение 16, строение 3", - "mismatches": { - "street": { - "expected": "2-я линия Хорошёвского Серебряного Бора", - "actual": "я линия Хорошёвского Серебряного Бора, домовладение" - } - } - }, - { - "source_row": 18687, - "fias_id": "9be0d9d1-1f10-45ed-872f-c7c21594f33f", - "tier": "house_only", - "raw": "МЖД, Киевское, 2-й километр, дом 9", - "mismatches": { - "street": { - "expected": "МЖД, Киевское, 2-й километр", - "actual": "Киевское" - } - } - }, - { - "source_row": 19022, - "fias_id": "b3558a04-8ef9-4d17-afed-34d0ea5a3e0e", - "tier": "house_structure", - "raw": "Никольская улица, дом 11-13, строение 2", - "mismatches": { - "house_num": { - "expected": "11-13", - "actual": "11" - } - } - }, - { - "source_row": 20406, - "fias_id": "04bc1903-12ed-4297-a897-c8fd2547e196", - "tier": "house_corpus", - "raw": "9-я Северная линия, дом 15, корпус 3", - "mismatches": { - "street": { - "expected": "9-я Северная линия", - "actual": "линия" - } - } - }, - { - "source_row": 20711, - "fias_id": "0f98146a-2fa6-451c-831e-86ebd3d44968", - "tier": "house_only", - "raw": "17-й проезд Марьиной Рощи, дом 6А", - "mismatches": { - "street": { - "expected": "17-й проезд Марьиной Рощи", - "actual": "17-й проезд" - } - } - }, - { - "source_row": 21116, - "fias_id": "dd8f3a1f-6734-4e73-91a7-8c60832b51db", - "tier": "house_structure", - "raw": "2-я линия Хорошёвского Серебряного Бора, дом 47, строение 13", - "mismatches": { - "street": { - "expected": "2-я линия Хорошёвского Серебряного Бора", - "actual": "я линия Хорошёвского Серебряного Бора" - } - } - }, - { - "source_row": 21408, - "fias_id": "219ccd89-d02d-4ef1-9c3b-833b5619f503", - "tier": "house_structure", - "raw": "Ленинградский проспект, дом 47, строение А", - "mismatches": { - "structure": { - "expected": "А", - "actual": null - } - } - }, - { - "source_row": 21961, - "fias_id": "611dcb5a-4bd1-40c8-a037-1df4c121efaa", - "tier": "house_structure", - "raw": "Бакунинская улица, дом 62-68, строение 1", - "mismatches": { - "house_num": { - "expected": "62-68", - "actual": "62" - } - } - }, - { - "source_row": 25059, - "fias_id": "17d52287-c6e6-4f94-a4ef-951343b61698", - "tier": "house_structure", - "raw": "6-я улица Лазенки, дом 2, строение 16", - "mismatches": { - "street": { - "expected": "6-я улица Лазенки", - "actual": "улица Лазенки" - } - } - }, - { - "source_row": 25157, - "fias_id": "d34fd4f8-3c08-4fa2-b222-838ae30f486c", - "tier": "house_structure", - "raw": "1-я линия Хорошёвского Серебряного Бора, домовладение 4, строение 1", - "mismatches": { - "street": { - "expected": "1-я линия Хорошёвского Серебряного Бора", - "actual": "я линия Хорошёвского Серебряного Бора, домовладение" - } - } - }, - { - "source_row": 26886, - "fias_id": "681791f2-daeb-4c1f-86c0-7e302864531c", - "tier": "house_structure", - "raw": "1-й Лучевой просек, дом 7, строение 3", - "mismatches": { - "street": { - "expected": "1-й Лучевой просек", - "actual": "й Лучевой" - } - } - }, - { - "source_row": 27635, - "fias_id": "edd3b776-9e4c-49e8-8495-27a906b51d82", - "tier": "house_structure", - "raw": "5-й Лучевой просек, дом 3, строение 1", - "mismatches": { - "street": { - "expected": "5-й Лучевой просек", - "actual": "й Лучевой" - } - } - }, - { - "source_row": 29730, - "fias_id": "3e7116b5-f6a3-4581-a473-b17853bab618", - "tier": "house_structure", - "raw": "1-я улица Измайловского Зверинца, дом 19, строение 22", - "mismatches": { - "street": { - "expected": "1-я улица Измайловского Зверинца", - "actual": "улица Измайловского Зверинца" - } - } - }, - { - "source_row": 30057, - "fias_id": "e6e92639-57ea-47d8-a687-048e24ac626c", - "tier": "house_structure", - "raw": "МЖД, Киевское, 1-й километр, дом 3, строение 5", - "mismatches": { - "street": { - "expected": "МЖД, Киевское, 1-й километр", - "actual": "Киевское" - } - } - }, - { - "source_row": 30291, - "fias_id": "2f1dc8bc-0410-4e4b-b9b7-5193e82e17aa", - "tier": "house_only", - "raw": "6-я улица Лазенки, дом 30", - "mismatches": { - "street": { - "expected": "6-я улица Лазенки", - "actual": "улица Лазенки" - } - } - }, - { - "source_row": 30712, - "fias_id": "856d5b9f-a623-4772-9be0-38bfdcf2e996", - "tier": "house_only", - "raw": "5-й квартал Капотня, дом 13", - "mismatches": { - "street": { - "expected": "5-й квартал Капотня", - "actual": "й квартал Капотня" - } - } - }, - { - "source_row": 31192, - "fias_id": "be5bdb1f-916a-4b3c-a39b-bbd96aae2b3b", - "tier": "house_only", - "raw": "1-й проезд Марьиной Рощи, дом 7/9", - "mismatches": { - "street": { - "expected": "1-й проезд Марьиной Рощи", - "actual": "1-й проезд" - } - } - }, - { - "source_row": 31424, - "fias_id": "4f9042fc-77a6-4cec-b575-e42198e9e034", - "tier": "house_only", - "raw": "1-й Лучевой просек, дом 2А", - "mismatches": { - "street": { - "expected": "1-й Лучевой просек", - "actual": "й Лучевой" - } - } - }, - { - "source_row": 32210, - "fias_id": "9b84618c-68ce-444f-98e5-2dcdec402386", - "tier": "house_only", - "raw": "город Московский, 1-й микрорайон, дом 5А", - "mismatches": { - "street": { - "expected": "1-й микрорайон", - "actual": "й микрорайон" - } - } - }, - { - "source_row": 32494, - "fias_id": "d7b73a4d-7076-483e-90a1-a4e848a172bd", - "tier": "house_only", - "raw": "улица Мичуринский Проспект, Олимпийская Деревня, дом 17", - "mismatches": { - "street": { - "expected": "улица Мичуринский Проспект, Олимпийская Деревня", - "actual": "улица Мичуринский" - } - } - }, - { - "source_row": 33422, - "fias_id": "af57bd99-98ad-476e-b0f1-81a984c7214c", - "tier": "house_structure", - "raw": "4-й проезд Подбельского, дом 3, строение 11", - "mismatches": { - "street": { - "expected": "4-й проезд Подбельского", - "actual": "4-й проезд" - } - } - }, - { - "source_row": 33696, - "fias_id": "f64f6dc4-bf59-41d7-a8e3-d9c16bae14b0", - "tier": "house_structure", - "raw": "Майский просек, дом 7, строение 1", - "mismatches": { - "street": { - "expected": "Майский просек", - "actual": "Майский" - } - } - }, - { - "source_row": 34304, - "fias_id": "7f0f4f05-271b-4ba9-aa34-b77bdcf62805", - "tier": "house_only", - "raw": "поселение Рязановское, деревня Никульское, микрорайон \"Петровская Слобода\", дом 7", - "mismatches": { - "street": { - "expected": "микрорайон \"Петровская Слобода\"", - "actual": "микрорайон" - } - } - } - ], - "slices": { - "house_only": { - "rows": 7801, - "tiers": { - "house_only": 7801 - }, - "matching": "case-insensitive exact component value after whitespace and ё/е folding; street includes its source type marker", - "micro": { - "tp": 12715, - "fp": 2862, - "fn": 2887, - "support": 15602, - "precision": 0.816268, - "recall": 0.81496, - "f1": 0.815613 - }, - "macro_field_f1": 0.407909, - "exact_address_rate": 0.630304, - "no_unparsed_rate": 0.237021, - "elapsed_seconds": 3.465, - "rows_per_second": 2251.5 - }, - "house_corpus": { - "rows": 1326, - "tiers": { - "house_corpus": 1326 - }, - "matching": "case-insensitive exact component value after whitespace and ё/е folding; street includes its source type marker", - "micro": { - "tp": 3921, - "fp": 51, - "fn": 57, - "support": 3978, - "precision": 0.98716, - "recall": 0.985671, - "f1": 0.986415 - }, - "macro_field_f1": 0.739816, - "exact_address_rate": 0.957768, - "no_unparsed_rate": 0.846154, - "elapsed_seconds": 1.51, - "rows_per_second": 877.9 - }, - "house_structure": { - "rows": 5642, - "tiers": { - "house_structure": 5642 - }, - "matching": "case-insensitive exact component value after whitespace and ё/е folding; street includes its source type marker", - "micro": { - "tp": 14273, - "fp": 2648, - "fn": 2653, - "support": 16926, - "precision": 0.843508, - "recall": 0.843259, - "f1": 0.843383 - }, - "macro_field_f1": 0.632551, - "exact_address_rate": 0.578341, - "no_unparsed_rate": 0.49486, - "elapsed_seconds": 2.356, - "rows_per_second": 2394.8 - }, - "house_corpus_structure": { - "rows": 427, - "tiers": { - "house_corpus_structure": 427 - }, - "matching": "case-insensitive exact component value after whitespace and ё/е folding; street includes its source type marker", - "micro": { - "tp": 1695, - "fp": 11, - "fn": 13, - "support": 1708, - "precision": 0.993552, - "recall": 0.992389, - "f1": 0.99297 - }, - "macro_field_f1": 0.992973, - "exact_address_rate": 0.976581, - "no_unparsed_rate": 0.704918, - "elapsed_seconds": 1.473, - "rows_per_second": 289.9 - } - } -} diff --git a/evaluation/deepparse_data.py b/evaluation/deepparse_data.py deleted file mode 100644 index d6a76d6..0000000 --- a/evaluation/deepparse_data.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Pure helpers and pinned metadata for the Deepparse Russian address shard.""" - -from __future__ import annotations - -import hashlib -import json -from pathlib import Path -import re -from typing import Sequence - - -ROOT = Path(__file__).resolve().parents[1] -DATASET_REVISION = "cb61e5e49db87f8c3586b5494149f612460f8992" -DATASET_SHA256 = ( - "e61981a059967a1062fe445fa5ffe745a661f8b4afcec59a60c3cbb4f1444110" -) -DATASET_ROWS = 13_152_918 -DATASET_URL = ( - "https://huggingface.co/datasets/deepparse/worldwide-addresses/resolve/" - f"{DATASET_REVISION}/ru/chunk-0.parquet" -) -DEFAULT_SOURCE = ( - ROOT - / ".cache" - / "external" - / f"deepparse-worldwide-ru-{DATASET_REVISION[:8]}.parquet" -) -DEFAULT_FILTERED = ( - ROOT - / ".cache" - / "external" - / f"deepparse-ru-usable-{DATASET_REVISION[:8]}.parquet" -) -DEFAULT_SAMPLE = ( - ROOT - / ".cache" - / "external" - / f"deepparse-ru-test-100k-{DATASET_REVISION[:8]}.jsonl.gz" -) -DEFAULT_MANIFEST = ROOT / "evaluation" / "deepparse_manifest.json" - -SOURCE_TAGS = frozenset( - { - "Country", - "Province", - "County", - "District", - "Municipality", - "Suburb", - "PostalCode", - "StreetName", - "StreetNumber", - "Unit", - } -) -USEFUL_TAGS = frozenset({"StreetName", "StreetNumber", "Unit"}) -LABEL_MAP = { - "PostalCode": "POSTAL_CODE", - "Province": "REGION", - "County": "DISTRICT", - "District": "DISTRICT", - "Municipality": "CITY", - "StreetName": "STREET", - "StreetNumber": "HOUSE", - "Unit": "APARTMENT", -} -SCORED_LABELS = ( - "POSTAL_CODE", - "REGION", - "DISTRICT", - "CITY", - "STREET", - "HOUSE", - "APARTMENT", -) -EXPECTED_FIELDS = ( - "postal_code", - "region", - "district", - "city", - "street", - "house_num", - "apartment", -) -LABEL_FIELDS = { - "POSTAL_CODE": "postal_code", - "REGION": "region", - "DISTRICT": "district", - "CITY": "city", - "STREET": "street", - "HOUSE": "house_num", - "APARTMENT": "apartment", -} -_IDENTITY_TAGS = ( - "Province", - "County", - "District", - "Municipality", - "Suburb", - "StreetName", - "StreetNumber", -) -_SPACE_RE = re.compile(r"\s+") - - -def fold(value: str) -> str: - """Normalize only distinctions that are not useful address evidence.""" - - return _SPACE_RE.sub(" ", value.casefold().replace("ё", "е")).strip() - - -def normalized_address_id(address: str) -> bytes: - """Return a compact deterministic ID used for exact-text deduplication.""" - - return hashlib.blake2b( - fold(address).encode("utf-8"), - digest_size=16, - person=b"addr-example-v1", - ).digest() - - -def mapped_labels(tags: Sequence[str]) -> tuple[str, ...]: - return tuple(LABEL_MAP.get(tag, "O") for tag in tags) - - -def expected_components( - tokens: Sequence[str], - labels: Sequence[str], -) -> dict[str, str | None]: - values: dict[str, list[str]] = { - field: [] for field in EXPECTED_FIELDS - } - for token, label in zip(tokens, labels): - field = LABEL_FIELDS.get(label) - if field is not None: - values[field].append(token) - return { - field: " ".join(parts) if parts else None - for field, parts in values.items() - } - - -def quality_tier(tags: Sequence[str]) -> str: - present = set(tags) - if {"StreetName", "StreetNumber", "Unit"} <= present: - return "street_house_unit" - if {"StreetName", "StreetNumber"} <= present: - return "street_house" - if "StreetName" in present: - return "street_only" - return "number_or_unit_only" - - -def canonical_group_key( - tokens: Sequence[str], - tags: Sequence[str], -) -> bytes: - """Group formatting variants without leaking one building across splits.""" - - by_tag: dict[str, list[str]] = {tag: [] for tag in _IDENTITY_TAGS} - for token, tag in zip(tokens, tags): - if tag in by_tag: - by_tag[tag].append(token) - identity = [ - [tag, fold(" ".join(by_tag[tag]))] - for tag in _IDENTITY_TAGS - if by_tag[tag] - ] - if not identity: - identity = [["fallback", fold(" ".join(tokens))]] - return json.dumps( - identity, - ensure_ascii=False, - separators=(",", ":"), - ).encode("utf-8") - - -def group_id_and_split( - tokens: Sequence[str], - tags: Sequence[str], -) -> tuple[bytes, str]: - key = canonical_group_key(tokens, tags) - digest = hashlib.sha256(b"address-group-v1\0" + key).digest() - bucket = int.from_bytes(digest[:8], "big") % 10_000 - split = ( - "train" - if bucket < 9_000 - else "validation" - if bucket < 9_500 - else "test" - ) - return digest[:16], split - - -def sample_rank(example_id: bytes) -> int: - return int.from_bytes( - hashlib.sha256(b"benchmark-sample-v1\0" + example_id).digest()[:8], - "big", - ) diff --git a/evaluation/deepparse_manifest.json b/evaluation/deepparse_manifest.json deleted file mode 100644 index 864fe7b..0000000 --- a/evaluation/deepparse_manifest.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "format_version": 1, - "source": { - "repository": "deepparse/worldwide-addresses", - "configuration": "ru", - "license": "CC BY 4.0", - "revision": "cb61e5e49db87f8c3586b5494149f612460f8992", - "url": "https://huggingface.co/datasets/deepparse/worldwide-addresses/resolve/cb61e5e49db87f8c3586b5494149f612460f8992/ru/chunk-0.parquet", - "rows": 13152918, - "bytes": 371595309, - "sha256": "e61981a059967a1062fe445fa5ffe745a661f8b4afcec59a60c3cbb4f1444110" - }, - "policy": { - "purpose": "external clean-address training/evaluation corpus; not bundled in the runtime package", - "filter": "Russian rows with 1-64 whitespace tokens, known tags, matching token/tag lengths, and at least one StreetName, StreetNumber, or Unit tag", - "deduplication": "first row for each BLAKE2b-128 hash of case-folded, ё/е-folded, whitespace-normalized address text", - "grouping": "SHA-256 of structured province/county/district/municipality/suburb/street/house identity; unit and presentation fields are excluded so one building cannot cross splits", - "split": "group hash buckets: train 90%, validation 5%, test 5%", - "sample": "lowest deterministic SHA-256 ranks from the sealed test split", - "ignored_labels": [ - "Country", - "Suburb" - ] - }, - "filter_counts": { - "source_rows": 13152918, - "rejected_administrative_only": 5403275, - "eligible_rows": 7749643, - "unique_usable_rows": 6314158, - "duplicate_rows_removed": 1435485 - }, - "split_counts": { - "train": 5681842, - "validation": 316586, - "test": 315730 - }, - "tier_counts": { - "street_house_unit": 679076, - "street_house": 4614613, - "street_only": 984676, - "number_or_unit_only": 35793 - }, - "split_tier_counts": { - "test/number_or_unit_only": 1767, - "test/street_house": 230504, - "test/street_house_unit": 33894, - "test/street_only": 49565, - "train/number_or_unit_only": 32226, - "train/street_house": 4152669, - "train/street_house_unit": 611003, - "train/street_only": 885944, - "validation/number_or_unit_only": 1800, - "validation/street_house": 231440, - "validation/street_house_unit": 34179, - "validation/street_only": 49167 - }, - "artifacts": { - "filtered_parquet": { - "filename": "deepparse-ru-usable-cb61e5e4.parquet", - "rows": 6314158, - "bytes": 557321339, - "sha256": "2ca55950ea5a4d08fbfeb55d2f798adb1dcae1d4a6dd61372f6c707928e625e8" - }, - "test_sample_jsonl_gz": { - "filename": "deepparse-ru-test-100k-cb61e5e4.jsonl.gz", - "rows": 100000, - "bytes": 10981774, - "sha256": "349140469f42a73f95fb58c9c37b2148c57e3373fed5c0fc265397800c72022b" - } - }, - "limitations": [ - "The source is curated from open geographic address data and does not reproduce misspellings or punctuation-heavy user input.", - "Country and Suburb have no direct public package field and are kept as source context but mapped to O.", - "This deterministic test split becomes tuning data after its failures are used to change the parser; reserve another test source before making a final production claim." - ] -} diff --git a/evaluation/deepparse_report.json b/evaluation/deepparse_report.json deleted file mode 100644 index b2645ca..0000000 --- a/evaluation/deepparse_report.json +++ /dev/null @@ -1,2226 +0,0 @@ -{ - "scope": "untuned external evaluation on clean Russian open-geographic address strings from the sealed Deepparse test split", - "source": { - "repository": "deepparse/worldwide-addresses", - "configuration": "ru", - "license": "CC BY 4.0", - "revision": "cb61e5e49db87f8c3586b5494149f612460f8992" - }, - "limitations": [ - "clean registry-derived strings are easier than user-entered text", - "Country and Suburb source tags are retained as context but unscored", - "span overlap gives partial credit and is not exact-value accuracy" - ], - "rows": 100000, - "tiers": { - "number_or_unit_only": 543, - "street_house": 72789, - "street_house_unit": 10915, - "street_only": 15753 - }, - "matching": "one-to-one same-label character-span overlap", - "metric_definitions": { - "span_overlap_micro": "binary micro precision, recall, and F1 for one-to-one same-label spans with any character overlap", - "character_overlap_micro": "micro precision, recall, and F1 over same-label overlapping characters", - "token_label_micro": "micro precision, recall, and F1 over aligned source-token labels", - "exact_address_rate": "fraction of rows with identical labels and exact span boundaries", - "exact_token_sequence_rate": "fraction of rows whose complete aligned token-label sequence matches", - "fields": "per-field binary span-overlap metrics", - "character_fields": "per-field character-overlap metrics", - "token_fields": "per-field aligned token-label metrics" - }, - "span_overlap_micro": { - "tp": 222353, - "fp": 8494, - "fn": 73735, - "support": 296088, - "precision": 0.963205, - "recall": 0.750969, - "f1": 0.843948 - }, - "character_overlap_micro": { - "tp": 1932109, - "fp": 723321, - "fn": 1247100, - "support": 3179209, - "precision": 0.727607, - "recall": 0.607733, - "f1": 0.662289 - }, - "token_label_micro": { - "tp": 323165, - "fp": 104063, - "fn": 221611, - "support": 544776, - "precision": 0.756423, - "recall": 0.593207, - "f1": 0.664946 - }, - "micro": { - "tp": 222353, - "fp": 8494, - "fn": 73735, - "support": 296088, - "precision": 0.963205, - "recall": 0.750969, - "f1": 0.843948 - }, - "character_micro": { - "tp": 1932109, - "fp": 723321, - "fn": 1247100, - "support": 3179209, - "precision": 0.727607, - "recall": 0.607733, - "f1": 0.662289 - }, - "token_micro": { - "tp": 323165, - "fp": 104063, - "fn": 221611, - "support": 544776, - "precision": 0.756423, - "recall": 0.593207, - "f1": 0.664946 - }, - "macro_field_f1": 0.720482, - "token_macro_field_f1": 0.544346, - "character_macro_field_f1": 0.542689, - "exact_address_rate": 0.07826, - "exact_token_sequence_rate": 0.07958, - "exact_span_recall": 0.319189, - "overlap_spans": 222353, - "exact_spans": 94508, - "fields": { - "POSTAL_CODE": { - "tp": 13089, - "fp": 3, - "fn": 22, - "support": 13111, - "precision": 0.999771, - "recall": 0.998322, - "f1": 0.999046 - }, - "REGION": { - "tp": 1783, - "fp": 909, - "fn": 5646, - "support": 7429, - "precision": 0.662333, - "recall": 0.240005, - "f1": 0.352337 - }, - "DISTRICT": { - "tp": 1855, - "fp": 1687, - "fn": 5286, - "support": 7141, - "precision": 0.523715, - "recall": 0.259768, - "f1": 0.347281 - }, - "CITY": { - "tp": 33472, - "fp": 2985, - "fn": 40316, - "support": 73788, - "precision": 0.918123, - "recall": 0.453624, - "f1": 0.607229 - }, - "STREET": { - "tp": 97310, - "fp": 1227, - "fn": 2147, - "support": 99457, - "precision": 0.987548, - "recall": 0.978413, - "f1": 0.982959 - }, - "HOUSE": { - "tp": 65006, - "fp": 192, - "fn": 19241, - "support": 84247, - "precision": 0.997055, - "recall": 0.771612, - "f1": 0.869966 - }, - "APARTMENT": { - "tp": 9838, - "fp": 1491, - "fn": 1077, - "support": 10915, - "precision": 0.868391, - "recall": 0.901328, - "f1": 0.884553 - } - }, - "character_fields": { - "POSTAL_CODE": { - "tp": 78534, - "fp": 18, - "fn": 134, - "support": 78668, - "precision": 0.999771, - "recall": 0.998297, - "f1": 0.999033 - }, - "REGION": { - "tp": 18798, - "fp": 20807, - "fn": 110481, - "support": 129279, - "precision": 0.474637, - "recall": 0.145406, - "f1": 0.222614 - }, - "DISTRICT": { - "tp": 22816, - "fp": 30971, - "fn": 124975, - "support": 147791, - "precision": 0.424192, - "recall": 0.15438, - "f1": 0.226374 - }, - "CITY": { - "tp": 344460, - "fp": 108504, - "fn": 522138, - "support": 866598, - "precision": 0.760458, - "recall": 0.397485, - "f1": 0.522082 - }, - "STREET": { - "tp": 1289132, - "fp": 560389, - "fn": 222117, - "support": 1511249, - "precision": 0.697009, - "recall": 0.853024, - "f1": 0.767165 - }, - "HOUSE": { - "tp": 152417, - "fp": 561, - "fn": 211120, - "support": 363537, - "precision": 0.996333, - "recall": 0.419261, - "f1": 0.590175 - }, - "APARTMENT": { - "tp": 25952, - "fp": 2071, - "fn": 56135, - "support": 82087, - "precision": 0.926096, - "recall": 0.316152, - "f1": 0.471383 - } - }, - "token_fields": { - "POSTAL_CODE": { - "tp": 13066, - "fp": 3, - "fn": 46, - "support": 13112, - "precision": 0.99977, - "recall": 0.996492, - "f1": 0.998128 - }, - "REGION": { - "tp": 1783, - "fp": 2457, - "fn": 12638, - "support": 14421, - "precision": 0.420519, - "recall": 0.123639, - "f1": 0.191094 - }, - "DISTRICT": { - "tp": 2183, - "fp": 3789, - "fn": 14731, - "support": 16914, - "precision": 0.365539, - "recall": 0.129065, - "f1": 0.190772 - }, - "CITY": { - "tp": 42429, - "fp": 13150, - "fn": 77682, - "support": 120111, - "precision": 0.7634, - "recall": 0.353248, - "f1": 0.482998 - }, - "STREET": { - "tp": 189444, - "fp": 83066, - "fn": 24055, - "support": 213499, - "precision": 0.695182, - "recall": 0.88733, - "f1": 0.779591 - }, - "HOUSE": { - "tp": 65321, - "fp": 219, - "fn": 78917, - "support": 144238, - "precision": 0.996659, - "recall": 0.45287, - "f1": 0.622763 - }, - "APARTMENT": { - "tp": 8939, - "fp": 1379, - "fn": 13542, - "support": 22481, - "precision": 0.86635, - "recall": 0.397625, - "f1": 0.545078 - } - }, - "elapsed_seconds": 13.802, - "rows_per_second": 7245.4, - "failure_sample": [ - { - "source_row": 25, - "example_id": "0ef2d0933ea8999a251e4997bb399d73", - "tier": "street_house", - "raw": "Ярославская ул 74 Тутаев Россия", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 14 - }, - { - "label": "HOUSE", - "start": 15, - "end": 17 - }, - { - "label": "CITY", - "start": 18, - "end": 24 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 12, - "end": 24 - } - ] - }, - { - "source_row": 75, - "example_id": "fb85bcd05b3f82cf0e9779a0193a12fc", - "tier": "street_house", - "raw": "Заречная ул Дом 12", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 11 - }, - { - "label": "HOUSE", - "start": 12, - "end": 18 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 16, - "end": 18 - }, - { - "label": "STREET", - "start": 0, - "end": 11 - } - ] - }, - { - "source_row": 112, - "example_id": "753f4f717564efc1a557d3c5d5dc3a10", - "tier": "street_house_unit", - "raw": "Российская Федерация Ахуново переулок Хади Такташа Д 19 Квартира 255", - "gold": [ - { - "label": "CITY", - "start": 21, - "end": 28 - }, - { - "label": "STREET", - "start": 29, - "end": 50 - }, - { - "label": "HOUSE", - "start": 51, - "end": 55 - }, - { - "label": "APARTMENT", - "start": 56, - "end": 68 - } - ], - "predicted": [ - { - "label": "APARTMENT", - "start": 65, - "end": 68 - }, - { - "label": "CITY", - "start": 38, - "end": 42 - }, - { - "label": "HOUSE", - "start": 53, - "end": 55 - }, - { - "label": "STREET", - "start": 21, - "end": 37 - } - ] - }, - { - "source_row": 185, - "example_id": "14c22a474ec7770eb4151cea26b1dd72", - "tier": "street_house", - "raw": "Заречная улица 20 Селихино 681085", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 14 - }, - { - "label": "HOUSE", - "start": 15, - "end": 17 - }, - { - "label": "CITY", - "start": 18, - "end": 26 - }, - { - "label": "POSTAL_CODE", - "start": 27, - "end": 33 - } - ], - "predicted": [ - { - "label": "POSTAL_CODE", - "start": 27, - "end": 33 - }, - { - "label": "STREET", - "start": 9, - "end": 26 - } - ] - }, - { - "source_row": 351, - "example_id": "d6815ec47a560809a0750e874ffcf5fa", - "tier": "street_house", - "raw": "улица Карла Маркса Дом 22", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 18 - }, - { - "label": "HOUSE", - "start": 19, - "end": 25 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 23, - "end": 25 - }, - { - "label": "STREET", - "start": 0, - "end": 18 - } - ] - }, - { - "source_row": 459, - "example_id": "53fe8f4192061f5aa228c788ae05396a", - "tier": "street_house", - "raw": "ул 50 лет НЛМК 13 Липецк Российская Федерация", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 14 - }, - { - "label": "HOUSE", - "start": 15, - "end": 17 - }, - { - "label": "CITY", - "start": 18, - "end": 24 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 24 - } - ] - }, - { - "source_row": 554, - "example_id": "e3044af36d0e4d29592bb9b3891c8e48", - "tier": "street_house", - "raw": "Гобзянско-Наб улица Дом 64 Демидовское городское поселение", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 19 - }, - { - "label": "HOUSE", - "start": 20, - "end": 26 - }, - { - "label": "CITY", - "start": 27, - "end": 58 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 14, - "end": 19 - }, - { - "label": "HOUSE", - "start": 24, - "end": 26 - }, - { - "label": "STREET", - "start": 0, - "end": 13 - } - ] - }, - { - "source_row": 719, - "example_id": "0a1028a39fbb93973b3213678275b4cf", - "tier": "street_house", - "raw": "улица Собинова Дом 6", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 14 - }, - { - "label": "HOUSE", - "start": 15, - "end": 20 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 19, - "end": 20 - }, - { - "label": "STREET", - "start": 0, - "end": 14 - } - ] - }, - { - "source_row": 935, - "example_id": "7346a7b84cf7b13b7820e01d03d0ed71", - "tier": "street_house", - "raw": "ул Тимирязева Дом 41", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 13 - }, - { - "label": "HOUSE", - "start": 14, - "end": 20 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 18, - "end": 20 - }, - { - "label": "STREET", - "start": 0, - "end": 13 - } - ] - }, - { - "source_row": 972, - "example_id": "88de18541c9c213ed87bdd0b9f89ccff", - "tier": "street_house", - "raw": "ул Герцена 5 Надёжная Россия", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 10 - }, - { - "label": "HOUSE", - "start": 11, - "end": 12 - }, - { - "label": "CITY", - "start": 13, - "end": 21 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 21 - } - ] - }, - { - "source_row": 1048, - "example_id": "b253a343fe06d1e407120c5826531e18", - "tier": "street_house", - "raw": "Верхняя Казацкая улица Д 321 Курск Россия", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 22 - }, - { - "label": "HOUSE", - "start": 23, - "end": 28 - }, - { - "label": "CITY", - "start": 29, - "end": 34 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 29, - "end": 34 - }, - { - "label": "HOUSE", - "start": 25, - "end": 28 - }, - { - "label": "STREET", - "start": 0, - "end": 22 - } - ] - }, - { - "source_row": 1203, - "example_id": "393afc4700b66a31ea665c1aa1d30bf7", - "tier": "street_house", - "raw": "Российская Федерация Новосибирск Станционная улица Дом 30А к5", - "gold": [ - { - "label": "CITY", - "start": 21, - "end": 32 - }, - { - "label": "STREET", - "start": 33, - "end": 50 - }, - { - "label": "HOUSE", - "start": 51, - "end": 61 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 55, - "end": 61 - }, - { - "label": "STREET", - "start": 21, - "end": 50 - } - ] - }, - { - "source_row": 1402, - "example_id": "64be9ddaeea089712d5702e7e1f04113", - "tier": "street_house", - "raw": "Российская Федерация г. Новосибирск улица Дуси Ковальчук 77", - "gold": [ - { - "label": "CITY", - "start": 21, - "end": 35 - }, - { - "label": "STREET", - "start": 36, - "end": 56 - }, - { - "label": "HOUSE", - "start": 57, - "end": 59 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 24, - "end": 35 - }, - { - "label": "HOUSE", - "start": 57, - "end": 59 - }, - { - "label": "STREET", - "start": 36, - "end": 56 - } - ] - }, - { - "source_row": 1502, - "example_id": "dd0bdafc5ff93e81488b2d2cca8daee0", - "tier": "street_only", - "raw": "улица Чернышевского Усольское городское поселение", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 19 - }, - { - "label": "CITY", - "start": 20, - "end": 49 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 49 - } - ] - }, - { - "source_row": 1922, - "example_id": "8298dd67cefc682b5ed0f6f8793ec97a", - "tier": "street_house", - "raw": "5-я линия В.О. Д 18", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 14 - }, - { - "label": "HOUSE", - "start": 15, - "end": 19 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 10, - "end": 11 - }, - { - "label": "HOUSE", - "start": 17, - "end": 19 - }, - { - "label": "REGION", - "start": 4, - "end": 9 - }, - { - "label": "STREET", - "start": 12, - "end": 13 - } - ] - }, - { - "source_row": 1991, - "example_id": "7eb8588da250affca540b721bbb04dcf", - "tier": "street_only", - "raw": "1-й проезд улицы Сулакской Махачкала RU", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 26 - }, - { - "label": "CITY", - "start": 27, - "end": 36 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 27, - "end": 36 - }, - { - "label": "STREET", - "start": 0, - "end": 10 - } - ] - }, - { - "source_row": 2056, - "example_id": "0ac07b06a18a203bdc1688505e036845", - "tier": "street_house", - "raw": "Трубчевская улица Д 13", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 17 - }, - { - "label": "HOUSE", - "start": 18, - "end": 22 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 20, - "end": 22 - }, - { - "label": "STREET", - "start": 0, - "end": 17 - } - ] - }, - { - "source_row": 2092, - "example_id": "1d9c21906da8a733403f456c25ec698b", - "tier": "street_house", - "raw": "RU Ворошнево Тепличная улица Дом 29", - "gold": [ - { - "label": "CITY", - "start": 3, - "end": 12 - }, - { - "label": "STREET", - "start": 13, - "end": 28 - }, - { - "label": "HOUSE", - "start": 29, - "end": 35 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 33, - "end": 35 - }, - { - "label": "STREET", - "start": 0, - "end": 28 - } - ] - }, - { - "source_row": 2153, - "example_id": "094108d4c64d61693c22177d85dffd41", - "tier": "street_house", - "raw": "603010 г. Нижний Новгород Зелёная улица 39", - "gold": [ - { - "label": "POSTAL_CODE", - "start": 0, - "end": 6 - }, - { - "label": "CITY", - "start": 7, - "end": 25 - }, - { - "label": "STREET", - "start": 26, - "end": 39 - }, - { - "label": "HOUSE", - "start": 40, - "end": 42 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 10, - "end": 33 - }, - { - "label": "HOUSE", - "start": 40, - "end": 42 - }, - { - "label": "POSTAL_CODE", - "start": 0, - "end": 6 - }, - { - "label": "STREET", - "start": 34, - "end": 39 - } - ] - }, - { - "source_row": 2566, - "example_id": "b4f5840d7bf952bf5708985b3c75e2e7", - "tier": "street_only", - "raw": "Российская Федерация г. Новый Завод Нагорная улица", - "gold": [ - { - "label": "CITY", - "start": 21, - "end": 35 - }, - { - "label": "STREET", - "start": 36, - "end": 50 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 24, - "end": 44 - } - ] - }, - { - "source_row": 2711, - "example_id": "40e0f642423d104c8ba6ca75185d8170", - "tier": "street_house", - "raw": "Россия городской округ Кулебаки Кулебаки улица Рекордов Дом 6", - "gold": [ - { - "label": "DISTRICT", - "start": 7, - "end": 31 - }, - { - "label": "CITY", - "start": 32, - "end": 40 - }, - { - "label": "STREET", - "start": 41, - "end": 55 - }, - { - "label": "HOUSE", - "start": 56, - "end": 61 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 23, - "end": 31 - }, - { - "label": "HOUSE", - "start": 60, - "end": 61 - }, - { - "label": "STREET", - "start": 41, - "end": 55 - } - ] - }, - { - "source_row": 2727, - "example_id": "67cebe6ccbad6123d0ade16294adb22d", - "tier": "street_only", - "raw": "Школьная улица Ахмановское сельское поселение кировской области", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 14 - }, - { - "label": "CITY", - "start": 15, - "end": 45 - }, - { - "label": "REGION", - "start": 46, - "end": 63 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 9, - "end": 63 - } - ] - }, - { - "source_row": 2734, - "example_id": "5bf8d8a777b4c48f6f618650f9fbeec5", - "tier": "street_only", - "raw": "ул Николаева Грязи Россия", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 12 - }, - { - "label": "CITY", - "start": 13, - "end": 18 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 18 - } - ] - }, - { - "source_row": 2740, - "example_id": "dc00252f73378e1ba52b1ae537f316ca", - "tier": "street_only", - "raw": "ул Октябрьской Революции Куса Россия 456940", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 24 - }, - { - "label": "CITY", - "start": 25, - "end": 29 - }, - { - "label": "POSTAL_CODE", - "start": 37, - "end": 43 - } - ], - "predicted": [ - { - "label": "POSTAL_CODE", - "start": 37, - "end": 43 - }, - { - "label": "STREET", - "start": 0, - "end": 29 - } - ] - }, - { - "source_row": 2914, - "example_id": "86f1bd6f336ee2db3e259f573e4e6415", - "tier": "street_house", - "raw": "Пионерский переулок Д 9", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 19 - }, - { - "label": "HOUSE", - "start": 20, - "end": 23 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 22, - "end": 23 - }, - { - "label": "STREET", - "start": 0, - "end": 19 - } - ] - }, - { - "source_row": 2920, - "example_id": "1ae9fdf582a580ce6512c4c295425c4e", - "tier": "street_house", - "raw": "ул Красина Дом 7 Махачкала Россия", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 10 - }, - { - "label": "HOUSE", - "start": 11, - "end": 16 - }, - { - "label": "CITY", - "start": 17, - "end": 26 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 15, - "end": 16 - }, - { - "label": "STREET", - "start": 0, - "end": 10 - } - ] - }, - { - "source_row": 2975, - "example_id": "85c023512fde9f1fac7ce2920872f94f", - "tier": "street_house", - "raw": "ул Гагарина Дом 34", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 11 - }, - { - "label": "HOUSE", - "start": 12, - "end": 18 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 16, - "end": 18 - }, - { - "label": "STREET", - "start": 0, - "end": 11 - } - ] - }, - { - "source_row": 3184, - "example_id": "ad88d01645f48170c9aee1ee0624abe3", - "tier": "street_only", - "raw": "403533 Россия г Фролово улица Красина", - "gold": [ - { - "label": "POSTAL_CODE", - "start": 0, - "end": 6 - }, - { - "label": "CITY", - "start": 14, - "end": 23 - }, - { - "label": "STREET", - "start": 24, - "end": 37 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 16, - "end": 23 - }, - { - "label": "POSTAL_CODE", - "start": 0, - "end": 6 - }, - { - "label": "STREET", - "start": 24, - "end": 37 - } - ] - }, - { - "source_row": 3518, - "example_id": "fc9a48db713540e8d5001fdfbe1a72b2", - "tier": "street_house", - "raw": "улица Физкультурников Д 5", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 21 - }, - { - "label": "HOUSE", - "start": 22, - "end": 25 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 24, - "end": 25 - }, - { - "label": "STREET", - "start": 0, - "end": 21 - } - ] - }, - { - "source_row": 3600, - "example_id": "fbc2cdb8c2cfa5f643a73769a5284288", - "tier": "street_house", - "raw": "Паровозный переулок Дом 32", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 19 - }, - { - "label": "HOUSE", - "start": 20, - "end": 26 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 24, - "end": 26 - }, - { - "label": "STREET", - "start": 0, - "end": 19 - } - ] - }, - { - "source_row": 3690, - "example_id": "82638e809f6ce79661ab19d7b4b905f4", - "tier": "street_only", - "raw": "Россия городское поселение Щёлково проспект 60 лет Октября", - "gold": [ - { - "label": "CITY", - "start": 7, - "end": 34 - }, - { - "label": "STREET", - "start": 35, - "end": 58 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 47, - "end": 50 - }, - { - "label": "STREET", - "start": 7, - "end": 43 - } - ] - }, - { - "source_row": 3717, - "example_id": "87f7c5162817c674fcdaaa93de560ddd", - "tier": "street_house", - "raw": "Октябрьская улица 37 г. Архангельск", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 17 - }, - { - "label": "HOUSE", - "start": 18, - "end": 20 - }, - { - "label": "CITY", - "start": 21, - "end": 35 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 24, - "end": 35 - }, - { - "label": "STREET", - "start": 0, - "end": 17 - } - ] - }, - { - "source_row": 3932, - "example_id": "308f25b2aaf7dc1acfd6dd21b5f89083", - "tier": "street_only", - "raw": "Мира Чуровское сельское поселение Россия", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 4 - }, - { - "label": "CITY", - "start": 5, - "end": 33 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 15, - "end": 23 - }, - { - "label": "STREET", - "start": 0, - "end": 14 - } - ] - }, - { - "source_row": 4307, - "example_id": "340ada078479e858b9d164ffb59fc334", - "tier": "street_house", - "raw": "Покровская улица Дом 149 Старый Оскол Россия", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 16 - }, - { - "label": "HOUSE", - "start": 17, - "end": 24 - }, - { - "label": "CITY", - "start": 25, - "end": 37 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 21, - "end": 24 - }, - { - "label": "STREET", - "start": 11, - "end": 37 - } - ] - }, - { - "source_row": 4428, - "example_id": "643fc58bf2ef03548600e3c697038239", - "tier": "street_house", - "raw": "Российская Федерация городского округа химок г. Химки улица Горшина 6 к2", - "gold": [ - { - "label": "DISTRICT", - "start": 21, - "end": 44 - }, - { - "label": "CITY", - "start": 45, - "end": 53 - }, - { - "label": "STREET", - "start": 54, - "end": 67 - }, - { - "label": "HOUSE", - "start": 68, - "end": 72 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 48, - "end": 53 - }, - { - "label": "HOUSE", - "start": 68, - "end": 72 - }, - { - "label": "STREET", - "start": 54, - "end": 67 - } - ] - }, - { - "source_row": 4705, - "example_id": "5916d39af436559b3a3938d7a6bf0ee2", - "tier": "street_house", - "raw": "Северограничная 28А Струги Красные Россия 181110", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 15 - }, - { - "label": "HOUSE", - "start": 16, - "end": 19 - }, - { - "label": "CITY", - "start": 20, - "end": 34 - }, - { - "label": "POSTAL_CODE", - "start": 42, - "end": 48 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 20, - "end": 26 - }, - { - "label": "POSTAL_CODE", - "start": 42, - "end": 48 - }, - { - "label": "REGION", - "start": 0, - "end": 15 - }, - { - "label": "STREET", - "start": 27, - "end": 34 - } - ] - }, - { - "source_row": 4805, - "example_id": "3890e9e2616a84d7fefb2d9d4abbc86e", - "tier": "street_house", - "raw": "Благовещенская ул Дом 4", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 17 - }, - { - "label": "HOUSE", - "start": 18, - "end": 23 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 22, - "end": 23 - }, - { - "label": "STREET", - "start": 0, - "end": 17 - } - ] - }, - { - "source_row": 4809, - "example_id": "27c38d8b1e3f5adb1260101ca4137fc2", - "tier": "street_house_unit", - "raw": "Парижский переулок 24 Кв 504 Троицкий Российская Федерация", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 18 - }, - { - "label": "HOUSE", - "start": 19, - "end": 21 - }, - { - "label": "APARTMENT", - "start": 22, - "end": 28 - }, - { - "label": "CITY", - "start": 29, - "end": 37 - } - ], - "predicted": [ - { - "label": "APARTMENT", - "start": 25, - "end": 28 - }, - { - "label": "HOUSE", - "start": 19, - "end": 21 - }, - { - "label": "STREET", - "start": 0, - "end": 18 - } - ] - }, - { - "source_row": 4861, - "example_id": "e003bd1043f64461b530b51479f770c8", - "tier": "street_house", - "raw": "Российская Федерация Новокузнецк улица Клименко Дом 27", - "gold": [ - { - "label": "CITY", - "start": 21, - "end": 32 - }, - { - "label": "STREET", - "start": 33, - "end": 47 - }, - { - "label": "HOUSE", - "start": 48, - "end": 54 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 21, - "end": 32 - }, - { - "label": "HOUSE", - "start": 52, - "end": 54 - }, - { - "label": "STREET", - "start": 33, - "end": 47 - } - ] - }, - { - "source_row": 5001, - "example_id": "5591203637c0dcab933867269287e3a9", - "tier": "street_house", - "raw": "Шарташская улица 4 Екатеринбург Российская Федерация", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 16 - }, - { - "label": "HOUSE", - "start": 17, - "end": 18 - }, - { - "label": "CITY", - "start": 19, - "end": 31 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 11, - "end": 31 - } - ] - }, - { - "source_row": 5067, - "example_id": "8a54ea3bfea9a8df538a54ad325cd23c", - "tier": "street_house", - "raw": "Крестьянская улица Дом 47 г Михайловское сельское поселение Россия", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 18 - }, - { - "label": "HOUSE", - "start": 19, - "end": 25 - }, - { - "label": "CITY", - "start": 26, - "end": 59 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 28, - "end": 59 - }, - { - "label": "HOUSE", - "start": 23, - "end": 25 - }, - { - "label": "STREET", - "start": 0, - "end": 18 - } - ] - }, - { - "source_row": 5192, - "example_id": "9e8970e3c3908da4dcf78519464dfe5c", - "tier": "street_house", - "raw": "Светлоярская улица Д 50 г. Волгоград 400029", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 18 - }, - { - "label": "HOUSE", - "start": 19, - "end": 23 - }, - { - "label": "CITY", - "start": 24, - "end": 36 - }, - { - "label": "POSTAL_CODE", - "start": 37, - "end": 43 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 27, - "end": 36 - }, - { - "label": "HOUSE", - "start": 21, - "end": 23 - }, - { - "label": "POSTAL_CODE", - "start": 37, - "end": 43 - }, - { - "label": "STREET", - "start": 0, - "end": 18 - } - ] - }, - { - "source_row": 5276, - "example_id": "c42b3e01f1a8b2b9bad5226dc0f4aa45", - "tier": "street_house", - "raw": "ул Салтыкова-Щедрина 3А Ярославль Российская Федерация 150000", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 20 - }, - { - "label": "HOUSE", - "start": 21, - "end": 23 - }, - { - "label": "CITY", - "start": 24, - "end": 33 - }, - { - "label": "POSTAL_CODE", - "start": 55, - "end": 61 - } - ], - "predicted": [ - { - "label": "POSTAL_CODE", - "start": 55, - "end": 61 - }, - { - "label": "STREET", - "start": 0, - "end": 33 - } - ] - }, - { - "source_row": 5452, - "example_id": "349e2410b67271cc34a4ee1c04fe825b", - "tier": "street_house", - "raw": "Советский проспект Д 46 Березники", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 18 - }, - { - "label": "HOUSE", - "start": 19, - "end": 23 - }, - { - "label": "CITY", - "start": 24, - "end": 33 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 24, - "end": 33 - }, - { - "label": "HOUSE", - "start": 21, - "end": 23 - }, - { - "label": "STREET", - "start": 0, - "end": 18 - } - ] - }, - { - "source_row": 5580, - "example_id": "621efcf5e26f100c815da93bb11b9319", - "tier": "street_house", - "raw": "Солнечная улица Д 224", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 15 - }, - { - "label": "HOUSE", - "start": 16, - "end": 21 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 18, - "end": 21 - }, - { - "label": "STREET", - "start": 0, - "end": 15 - } - ] - }, - { - "source_row": 5710, - "example_id": "9d135105352d7ccd6ad7b445ab87b52c", - "tier": "street_only", - "raw": "Российская Федерация г Смоленск Ремесленная улица", - "gold": [ - { - "label": "CITY", - "start": 21, - "end": 31 - }, - { - "label": "STREET", - "start": 32, - "end": 49 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 23, - "end": 43 - } - ] - }, - { - "source_row": 5820, - "example_id": "582df1c353aff489aa200010c61bd0a3", - "tier": "street_house", - "raw": "улица Гайдара Д 20 г Сафоновское сельское поселение", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 13 - }, - { - "label": "HOUSE", - "start": 14, - "end": 18 - }, - { - "label": "CITY", - "start": 19, - "end": 51 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 21, - "end": 51 - }, - { - "label": "HOUSE", - "start": 16, - "end": 18 - }, - { - "label": "STREET", - "start": 0, - "end": 13 - } - ] - }, - { - "source_row": 5945, - "example_id": "2c69b4beba1a71bff31f83ea0f60cfee", - "tier": "street_house_unit", - "raw": "Лесная ул 1 Кв 1", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 9 - }, - { - "label": "HOUSE", - "start": 10, - "end": 11 - }, - { - "label": "APARTMENT", - "start": 12, - "end": 16 - } - ], - "predicted": [ - { - "label": "APARTMENT", - "start": 15, - "end": 16 - }, - { - "label": "HOUSE", - "start": 10, - "end": 11 - }, - { - "label": "STREET", - "start": 0, - "end": 9 - } - ] - }, - { - "source_row": 6302, - "example_id": "93f6085dba835019900b9236f3aa365d", - "tier": "street_only", - "raw": "площадь Обороны г Санкт Петербург", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 15 - }, - { - "label": "CITY", - "start": 16, - "end": 33 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 18, - "end": 33 - }, - { - "label": "STREET", - "start": 0, - "end": 15 - } - ] - }, - { - "source_row": 6329, - "example_id": "55a13171dbddd70785ab3220c4316246", - "tier": "street_house", - "raw": "Дом 18 1-я ул Володарского", - "gold": [ - { - "label": "HOUSE", - "start": 0, - "end": 6 - }, - { - "label": "STREET", - "start": 7, - "end": 26 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 4, - "end": 6 - }, - { - "label": "STREET", - "start": 11, - "end": 26 - } - ] - } - ], - "slices": { - "street_house_unit": { - "rows": 10915, - "tiers": { - "street_house_unit": 10915 - }, - "matching": "one-to-one same-label character-span overlap", - "micro": { - "tp": 35073, - "fp": 1018, - "fn": 5279, - "support": 40352, - "precision": 0.971794, - "recall": 0.869176, - "f1": 0.917625 - }, - "character_micro": { - "tp": 227885, - "fp": 62424, - "fn": 160582, - "support": 388467, - "precision": 0.784974, - "recall": 0.586626, - "f1": 0.671459 - }, - "token_micro": { - "tp": 43996, - "fp": 10864, - "fn": 32490, - "support": 76486, - "precision": 0.801969, - "recall": 0.575216, - "f1": 0.669925 - }, - "macro_field_f1": 0.777179, - "token_macro_field_f1": 0.573404, - "character_macro_field_f1": 0.574805, - "exact_address_rate": 0.0, - "exact_token_sequence_rate": 0.0, - "exact_span_recall": 0.318002, - "overlap_spans": 35073, - "exact_spans": 12832, - "elapsed_seconds": 2.437, - "rows_per_second": 4479.5 - }, - "street_house": { - "rows": 72789, - "tiers": { - "street_house": 72789 - }, - "matching": "one-to-one same-label character-span overlap", - "micro": { - "tp": 162379, - "fp": 5852, - "fn": 54440, - "support": 216819, - "precision": 0.965214, - "recall": 0.748915, - "f1": 0.843418 - }, - "character_micro": { - "tp": 1427011, - "fp": 472848, - "fn": 835135, - "support": 2262146, - "precision": 0.751114, - "recall": 0.630822, - "f1": 0.685732 - }, - "token_micro": { - "tp": 239188, - "fp": 72217, - "fn": 155801, - "support": 394989, - "precision": 0.768093, - "recall": 0.605556, - "f1": 0.677208 - }, - "macro_field_f1": 0.602626, - "token_macro_field_f1": 0.472866, - "character_macro_field_f1": 0.484751, - "exact_address_rate": 0.098215, - "exact_token_sequence_rate": 0.099452, - "exact_span_recall": 0.334094, - "overlap_spans": 162379, - "exact_spans": 72438, - "elapsed_seconds": 10.453, - "rows_per_second": 6963.4 - }, - "street_only": { - "rows": 15753, - "tiers": { - "street_only": 15753 - }, - "matching": "one-to-one same-label character-span overlap", - "micro": { - "tp": 24041, - "fp": 1359, - "fn": 13590, - "support": 37631, - "precision": 0.946496, - "recall": 0.638862, - "f1": 0.762831 - }, - "character_micro": { - "tp": 272138, - "fp": 185047, - "fn": 244701, - "support": 516839, - "precision": 0.595247, - "recall": 0.526543, - "f1": 0.558791 - }, - "token_micro": { - "tp": 39026, - "fp": 20541, - "fn": 31920, - "support": 70946, - "precision": 0.655161, - "recall": 0.55008, - "f1": 0.59804 - }, - "macro_field_f1": 0.427581, - "token_macro_field_f1": 0.347224, - "character_macro_field_f1": 0.348103, - "exact_address_rate": 0.042976, - "exact_token_sequence_rate": 0.045642, - "exact_span_recall": 0.241184, - "overlap_spans": 24041, - "exact_spans": 9076, - "elapsed_seconds": 2.995, - "rows_per_second": 5259.3 - }, - "number_or_unit_only": { - "rows": 543, - "tiers": { - "number_or_unit_only": 543 - }, - "matching": "one-to-one same-label character-span overlap", - "micro": { - "tp": 860, - "fp": 265, - "fn": 426, - "support": 1286, - "precision": 0.764444, - "recall": 0.66874, - "f1": 0.713397 - }, - "character_micro": { - "tp": 5075, - "fp": 3002, - "fn": 6682, - "support": 11757, - "precision": 0.628327, - "recall": 0.431658, - "f1": 0.511748 - }, - "token_micro": { - "tp": 955, - "fp": 441, - "fn": 1400, - "support": 2355, - "precision": 0.684097, - "recall": 0.40552, - "f1": 0.509198 - }, - "macro_field_f1": 0.526859, - "token_macro_field_f1": 0.39145, - "character_macro_field_f1": 0.407957, - "exact_address_rate": 0.0, - "exact_token_sequence_rate": 0.0, - "exact_span_recall": 0.125972, - "overlap_spans": 860, - "exact_spans": 162, - "elapsed_seconds": 0.718, - "rows_per_second": 755.8 - } - } -} diff --git a/evaluation/detection_reference.jsonl b/evaluation/detection_reference.jsonl deleted file mode 100644 index e22edc6..0000000 --- a/evaluation/detection_reference.jsonl +++ /dev/null @@ -1,30 +0,0 @@ -{"id":"detect-001","message":"Курьер приедет по адресу: Москва, ул. Тверская, д. 13, кв. 4. Позвоните заранее.","expected":["Москва, ул. Тверская, д. 13, кв. 4"],"scenario_family":"positive_explicit","context_style":"sentence_with_address_cue","address_style":"city_street_house_unit","boundary_style":"period_after_number","polarity":"positive","ambiguity":"low","notes":"Cue should be excluded; city should be retained."} -{"id":"detect-002","message":"Напиши мне, когда будешь на ул. Тверской, д. 13.","expected":["ул. Тверской, д. 13"],"scenario_family":"positive_explicit","context_style":"prose_prefix","address_style":"street_house","boundary_style":"terminal_period","polarity":"positive","ambiguity":"low","notes":"Leading prose must not enter the span."} -{"id":"detect-003","message":"Встречаемся: Тверская улица, дом 13 завтра после шести.","expected":["Тверская улица, дом 13"],"scenario_family":"positive_suffix_marker","context_style":"colon_prefix_and_trailing_prose","address_style":"suffix_street_house","boundary_style":"house_before_trailing_words","polarity":"positive","ambiguity":"medium","notes":"Stop at the house value even without sentence punctuation."} -{"id":"detect-004","message":"Первый: ул. Ленина, д. 1; второй: ул. Мира, д. 2.","expected":["ул. Ленина, д. 1","ул. Мира, д. 2"],"scenario_family":"positive_multiple","context_style":"two_addresses","address_style":"street_house","boundary_style":"semicolon","polarity":"positive","ambiguity":"low","notes":"Return two ordered, non-overlapping spans."} -{"id":"detect-005","message":"Адрес: Ополченская 5-30","expected":["Ополченская 5-30"],"scenario_family":"positive_cue_unmarked","context_style":"address_cue","address_style":"unmarked_street_numeric_tail","boundary_style":"end_of_message","polarity":"positive","ambiguity":"high","notes":"The parser retains the compound-house alternative."} -{"id":"detect-006","message":"Доставка — ул.Тверская,д.13,кв.4, подъезд со двора","expected":["ул.Тверская,д.13,кв.4"],"scenario_family":"positive_compact","context_style":"label_and_trailing_instruction","address_style":"compact_punctuation","boundary_style":"comma_before_instruction","polarity":"positive","ambiguity":"low","notes":"No spaces after punctuation."} -{"id":"detect-007","message":"Отправьте документы: 300000, г. Тула, ул. Советская, д. 7. Спасибо.","expected":["300000, г. Тула, ул. Советская, д. 7"],"scenario_family":"positive_full","context_style":"sentence","address_style":"postal_city_street_house","boundary_style":"period_after_number","polarity":"positive","ambiguity":"low","notes":"Postal and city prefix belong to the address."} -{"id":"detect-008","message":"Точка выдачи:\nул.\u00a0Мира,\u00a0д.\u00a05\nработает до 20:00","expected":["ул.\u00a0Мира,\u00a0д.\u00a05"],"scenario_family":"positive_unicode","context_style":"multiline","address_style":"unicode_whitespace","boundary_style":"newline","polarity":"positive","ambiguity":"low","notes":"NBSP offsets must remain exact."} -{"id":"detect-009","message":"Офис находится здесь (проспект Мира, дом 10, офис 12), вход справа.","expected":["проспект Мира, дом 10, офис 12"],"scenario_family":"positive_parenthesized","context_style":"parentheses","address_style":"street_house_unit","boundary_style":"closing_parenthesis","polarity":"positive","ambiguity":"low","notes":"Parentheses are not part of the span."} -{"id":"detect-010","message":"Склад: ул. Южная, д. 2\nОфис: ул. Северная, д. 8","expected":["ул. Южная, д. 2","ул. Северная, д. 8"],"scenario_family":"positive_multiple","context_style":"multiline_two_addresses","address_style":"street_house","boundary_style":"newline","polarity":"positive","ambiguity":"low","notes":"Each line contains one address."} -{"id":"detect-011","message":"Заберите заказ на ул. Красной, д. 5-7.","expected":["ул. Красной, д. 5-7"],"scenario_family":"positive_compound_number","context_style":"prose_prefix","address_style":"hyphenated_house","boundary_style":"terminal_period","polarity":"positive","ambiguity":"medium","notes":"Compound number stays inside the detected span."} -{"id":"detect-012","message":"Мы находимся по адресу: ул. Полевая, д. 5/1, корп. 2.","expected":["ул. Полевая, д. 5/1, корп. 2"],"scenario_family":"positive_compound_number","context_style":"address_cue","address_style":"slash_house_corpus","boundary_style":"terminal_period","polarity":"positive","ambiguity":"low","notes":"Include slash house and корпус."} -{"id":"detect-013","message":"Проезд возможен до шоссе Энтузиастов, дом 29, корпус 4.","expected":["шоссе Энтузиастов, дом 29, корпус 4"],"scenario_family":"positive_street_type","context_style":"prose_prefix","address_style":"highway_house_corpus","boundary_style":"terminal_period","polarity":"positive","ambiguity":"low","notes":"Long street marker."} -{"id":"detect-014","message":"Жду у 2-й улицы Новосёлки, дом 11.","expected":["2-й улицы Новосёлки, дом 11"],"scenario_family":"positive_ordinal","context_style":"prose_prefix","address_style":"ordinal_suffix_street_house","boundary_style":"terminal_period","polarity":"positive","ambiguity":"medium","notes":"Ordinal prefix belongs to the street span."} -{"id":"detect-015","message":"Новый адрес офиса: г. Казань, улица Баумана, дом 3.","expected":["г. Казань, улица Баумана, дом 3"],"scenario_family":"positive_full","context_style":"address_cue","address_style":"city_street_house","boundary_style":"terminal_period","polarity":"positive","ambiguity":"low","notes":"Generic marked city prefix."} -{"id":"detect-016","message":"Оставьте у охраны: ул. 8 Марта, 5.","expected":["ул. 8 Марта, 5"],"scenario_family":"positive_bare_house","context_style":"prose_prefix","address_style":"numeric_street_bare_house","boundary_style":"terminal_period","polarity":"positive","ambiguity":"medium","notes":"Do not mistake 8 in the street name for the house."} -{"id":"detect-017","message":"В доме 13 квартир и два подъезда.","expected":[],"scenario_family":"negative_house_only","context_style":"ordinary_sentence","address_style":"none","boundary_style":"terminal_period","polarity":"negative","ambiguity":"low","notes":"House word and number are insufficient."} -{"id":"detect-018","message":"Встреча 13.05.2027 в 18:30.","expected":[],"scenario_family":"negative_datetime","context_style":"ordinary_sentence","address_style":"none","boundary_style":"terminal_period","polarity":"negative","ambiguity":"low","notes":"Dates and times are not addresses."} -{"id":"detect-019","message":"Я живу на улице Науки.","expected":[],"scenario_family":"negative_street_without_building","context_style":"ordinary_sentence","address_style":"street_without_house","boundary_style":"terminal_period","polarity":"negative","ambiguity":"medium","notes":"Street without a building is deliberately not detected."} -{"id":"detect-020","message":"Заказ № 4815 уже передан курьеру.","expected":[],"scenario_family":"negative_order_number","context_style":"ordinary_sentence","address_style":"none","boundary_style":"terminal_period","polarity":"negative","ambiguity":"low","notes":"Order numbers are not houses."} -{"id":"detect-021","message":"Заполните поле «Адрес» в форме.","expected":[],"scenario_family":"negative_cue_without_value","context_style":"instruction","address_style":"none","boundary_style":"terminal_period","polarity":"negative","ambiguity":"low","notes":"A cue without a parseable value is insufficient."} -{"id":"detect-022","message":"Нужно улучшить дом 13 и покрасить фасад.","expected":[],"scenario_family":"negative_lexical_overlap","context_style":"ordinary_sentence","address_style":"none","boundary_style":"terminal_period","polarity":"negative","ambiguity":"low","notes":"The substring «ул» inside a verb is not a marker."} -{"id":"detect-023","message":"Диапазон значений: 5-30, результат сохранён.","expected":[],"scenario_family":"negative_numeric_range","context_style":"ordinary_sentence","address_style":"none","boundary_style":"comma","polarity":"negative","ambiguity":"low","notes":"A numeric range without address evidence is insufficient."} -{"id":"detect-024","message":"Ополченская 5-30 указана в старом справочнике.","expected":[],"scenario_family":"negative_unmarked_without_cue","context_style":"ordinary_sentence","address_style":"unmarked_address_like","boundary_style":"terminal_period","polarity":"negative","ambiguity":"high","notes":"Conservative policy requires a cue or markers for unmarked text."} -{"id":"detect-025","message":"Реквизиты: УЛ . РОКОССОВСКОГО , дом 10 , офис 15 .","expected":["УЛ . РОКОССОВСКОГО , дом 10 , офис 15"],"scenario_family":"positive_tokenized_punctuation","context_style":"label_prefix","address_style":"spaces_around_periods","boundary_style":"terminal_period","polarity":"positive","ambiguity":"low","notes":"Spaces around abbreviation periods must not create sentence boundaries."} -{"id":"detect-026","message":"Администратор проверил IP-адресом d128:3c31:10d8:8ebf.","expected":[],"scenario_family":"negative_network_address","context_style":"technical_sentence","address_style":"ipv6","boundary_style":"terminal_period","polarity":"negative","ambiguity":"low","notes":"The lexical suffix in IP-адресом is not a postal address cue."} -{"id":"detect-027","message":"Ошибка соединения с IPv6 адресом 844e:4b9f:37c8:994e:b889.","expected":[],"scenario_family":"negative_network_address","context_style":"technical_sentence","address_style":"ipv6","boundary_style":"terminal_period","polarity":"negative","ambiguity":"low","notes":"A network-address cue must not activate postal detection."} -{"id":"detect-028","message":"Заявитель указал адрес, а также идентификационный номер 500100732259.","expected":[],"scenario_family":"negative_distant_number_after_cue","context_style":"ordinary_sentence","address_style":"none","boundary_style":"terminal_period","polarity":"negative","ambiguity":"medium","notes":"A later business identifier is not an unmarked address value."} -{"id":"detect-029","message":"Перерасход на проезд в командировки составил 666 тыс. руб.","expected":[],"scenario_family":"negative_street_marker_homonym","context_style":"ordinary_sentence","address_style":"none","boundary_style":"terminal_period","polarity":"negative","ambiguity":"medium","notes":"The ordinary noun «проезд» is not a street marker in this context."} -{"id":"detect-030","message":"Адрес: г . Чапаевск , ш . Ярцевская , д . 32 , кв . 4 .","expected":["г . Чапаевск , ш . Ярцевская , д . 32 , кв . 4"],"scenario_family":"positive_tokenized_punctuation","context_style":"address_cue","address_style":"spaces_around_all_periods","boundary_style":"terminal_period","polarity":"positive","ambiguity":"low","notes":"Separated abbreviation periods must remain inside one address clause."} diff --git a/evaluation/detection_report.json b/evaluation/detection_report.json deleted file mode 100644 index 39602eb..0000000 --- a/evaluation/detection_report.json +++ /dev/null @@ -1,153 +0,0 @@ -{ - "rows": 30, - "positive_rows": 18, - "negative_rows": 12, - "scope": "small, manually authored detection-behavior fixture; suitable for regression, not a production accuracy claim", - "metric_definitions": { - "exact_span_micro": "one-to-one precision, recall, and F1 requiring exact message boundaries", - "overlap_span_micro": "one-to-one precision, recall, and F1 requiring any character overlap; reported separately because it is lenient", - "exact_message_rate": "fraction of messages where the complete ordered span list is exact", - "negative_message_specificity": "fraction of annotated negative messages returning no spans" - }, - "exact_span_micro": { - "tp": 20, - "fp": 0, - "fn": 0, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "overlap_span_micro": { - "tp": 20, - "fp": 0, - "fn": 0, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "exact_message_rate": 1.0, - "negative_message_specificity": 1.0, - "scenarios": { - "negative_cue_without_value": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "negative_datetime": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "negative_distant_number_after_cue": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "negative_house_only": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "negative_lexical_overlap": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "negative_network_address": { - "rows": 2, - "exact_messages": 2, - "exact_message_rate": 1.0 - }, - "negative_numeric_range": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "negative_order_number": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "negative_street_marker_homonym": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "negative_street_without_building": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "negative_unmarked_without_cue": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "positive_bare_house": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "positive_compact": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "positive_compound_number": { - "rows": 2, - "exact_messages": 2, - "exact_message_rate": 1.0 - }, - "positive_cue_unmarked": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "positive_explicit": { - "rows": 2, - "exact_messages": 2, - "exact_message_rate": 1.0 - }, - "positive_full": { - "rows": 2, - "exact_messages": 2, - "exact_message_rate": 1.0 - }, - "positive_multiple": { - "rows": 2, - "exact_messages": 2, - "exact_message_rate": 1.0 - }, - "positive_ordinal": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "positive_parenthesized": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "positive_street_type": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "positive_suffix_marker": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - }, - "positive_tokenized_punctuation": { - "rows": 2, - "exact_messages": 2, - "exact_message_rate": 1.0 - }, - "positive_unicode": { - "rows": 1, - "exact_messages": 1, - "exact_message_rate": 1.0 - } - }, - "failure_sample": [] -} diff --git a/evaluation/evaluate_datamos.py b/evaluation/evaluate_datamos.py deleted file mode 100644 index 44c3110..0000000 --- a/evaluation/evaluate_datamos.py +++ /dev/null @@ -1,263 +0,0 @@ -"""Score exact building components on the filtered Moscow registry test split.""" - -from __future__ import annotations - -import argparse -from collections import Counter -import gzip -import json -from pathlib import Path -import sys -import time -from typing import Any, Callable, Iterable - - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT / "src")) - -from address_normalizer import parse -from address_normalizer.types import ParsedAddress - -from datamos_data import ( - DATASET_DATE, - DATASET_ID, - DATASET_VERSION, - DEFAULT_FILTERED, - FIELDS, - fold, -) - - -def _predicted_values(result: ParsedAddress) -> dict[str, str | None]: - street_parts = [ - part - for part in (result.street, result.street_type) - if part is not None - ] - street = ( - result.raw[ - min(part.start for part in street_parts) : - max(part.end for part in street_parts) - ] - if street_parts - else None - ) - return { - "street": street, - "house_num": result.house_num.value if result.house_num else None, - "corpus": result.corpus.value if result.corpus else None, - "structure": result.structure.value if result.structure else None, - } - - -def _metrics(counts: dict[str, int]) -> dict[str, int | float]: - precision = ( - counts["tp"] / (counts["tp"] + counts["fp"]) - if counts["tp"] + counts["fp"] - else 0.0 - ) - recall = ( - counts["tp"] / (counts["tp"] + counts["fn"]) - if counts["tp"] + counts["fn"] - else 0.0 - ) - f1 = ( - 2 * precision * recall / (precision + recall) - if precision + recall - else 0.0 - ) - return { - **counts, - "precision": round(precision, 6), - "recall": round(recall, 6), - "f1": round(f1, 6), - } - - -def score( - rows: Iterable[dict[str, Any]], - parse_address: Callable[[str], ParsedAddress] = parse, -) -> dict[str, Any]: - counts = { - field: {"tp": 0, "fp": 0, "fn": 0, "support": 0} - for field in FIELDS - } - failures: list[dict[str, Any]] = [] - tiers: Counter[str] = Counter() - row_count = exact_rows = no_unparsed_rows = 0 - started = time.monotonic() - for row in rows: - row_count += 1 - tiers[row["tier"]] += 1 - result = parse_address(row["raw"]) - predicted = _predicted_values(result) - mismatches: dict[str, dict[str, str | None]] = {} - for field in FIELDS: - wanted_value = row["expected"].get(field) - actual_value = predicted[field] - wanted = fold(wanted_value) if wanted_value is not None else None - actual = fold(actual_value) if actual_value is not None else None - field_counts = counts[field] - if wanted is not None: - field_counts["support"] += 1 - if wanted == actual: - if wanted is not None: - field_counts["tp"] += 1 - else: - if actual is not None: - field_counts["fp"] += 1 - if wanted is not None: - field_counts["fn"] += 1 - mismatches[field] = { - "expected": wanted_value, - "actual": actual_value, - } - if not mismatches: - exact_rows += 1 - elif len(failures) < 50: - failures.append( - { - "source_row": row["source_row"], - "fias_id": row["fias_id"], - "tier": row["tier"], - "raw": row["raw"], - "mismatches": mismatches, - } - ) - if not result.unparsed: - no_unparsed_rows += 1 - - fields = {field: _metrics(values) for field, values in counts.items()} - totals = { - key: sum(values[key] for values in counts.values()) - for key in ("tp", "fp", "fn", "support") - } - elapsed = time.monotonic() - started - exact_component_value_micro = _metrics(totals) - return { - "rows": row_count, - "tiers": dict(sorted(tiers.items())), - "matching": ( - "case-insensitive exact component value after whitespace and ё/е " - "folding; street includes its source type marker" - ), - "metric_definitions": { - "exact_component_value_micro": ( - "micro precision, recall, and F1 over case-insensitive exact " - "component values after whitespace and ё/е folding" - ), - "exact_address_rate": ( - "fraction of rows where every scored component value matches" - ), - "no_unparsed_rate": ( - "fraction of rows with no residual word or number spans" - ), - "fields": "per-field exact component-value metrics", - }, - "exact_component_value_micro": exact_component_value_micro, - # Retained for compatibility with the first published report. - "micro": exact_component_value_micro, - "macro_field_f1": round( - sum(float(value["f1"]) for value in fields.values()) - / len(fields), - 6, - ), - "exact_address_rate": round( - exact_rows / row_count if row_count else 0.0, - 6, - ), - "no_unparsed_rate": round( - no_unparsed_rows / row_count if row_count else 0.0, - 6, - ), - "fields": fields, - "elapsed_seconds": round(elapsed, 3), - "rows_per_second": round(row_count / elapsed, 1) if elapsed else None, - "failure_sample": failures, - } - - -def load_rows( - path: Path, - *, - split: str = "test", - tier: str | None = None, - limit: int | None = None, -) -> Iterable[dict[str, Any]]: - yielded = 0 - with gzip.open(path, "rt", encoding="utf-8") as source: - for line in source: - row = json.loads(line) - if row.get("split") != split: - continue - if tier is not None and row.get("tier") != tier: - continue - yield row - yielded += 1 - if limit is not None and yielded >= limit: - break - - -def _summary(report: dict[str, Any]) -> dict[str, Any]: - return { - key: value - for key, value in report.items() - if key - not in { - "failure_sample", - "fields", - "metric_definitions", - "exact_component_value_micro", - } - } - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--data", type=Path, default=DEFAULT_FILTERED) - parser.add_argument("--limit", type=int) - parser.add_argument("--output", type=Path) - args = parser.parse_args(argv) - if not args.data.exists(): - parser.error(f"{args.data} does not exist; run prepare_datamos.py first") - if args.limit is not None and args.limit <= 0: - parser.error("--limit must be positive") - - report = { - "scope": ( - "untuned exact-value evaluation on a group-disjoint test split of " - "active official Moscow registry building addresses" - ), - "source": { - "dataset_id": DATASET_ID, - "version": DATASET_VERSION, - "release_date": DATASET_DATE, - }, - "limitations": [ - "October 2021 snapshot; not current FIAS/GAR truth", - "Moscow-only clean legal/simplified address formatting", - "administrative fields and address existence resolution are unscored", - ], - **score(load_rows(args.data, limit=args.limit)), - "slices": { - tier: _summary( - score(load_rows(args.data, tier=tier, limit=args.limit)) - ) - for tier in ( - "house_only", - "house_corpus", - "house_structure", - "house_corpus_structure", - ) - }, - } - rendered = f"{json.dumps(report, ensure_ascii=False, indent=2)}\n" - if args.output: - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(rendered, encoding="utf-8") - print(rendered, end="") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/evaluation/evaluate_deepparse.py b/evaluation/evaluate_deepparse.py deleted file mode 100644 index 02e8f95..0000000 --- a/evaluation/evaluate_deepparse.py +++ /dev/null @@ -1,462 +0,0 @@ -"""Evaluate the parser on a prepared Deepparse Russian test sample.""" - -from __future__ import annotations - -import argparse -from collections import Counter -from dataclasses import asdict, dataclass -import gzip -import json -from pathlib import Path -import sys -import time -from typing import Any, Callable, Iterable, Sequence - - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT / "src")) - -from address_normalizer import parse -from address_normalizer.types import ParsedAddress - -from deepparse_data import ( - DATASET_REVISION, - DEFAULT_SAMPLE, - SCORED_LABELS, -) - - -@dataclass(frozen=True, slots=True) -class Span: - label: str - start: int - end: int - - -PREDICTED_FIELDS = { - "postal_code": "POSTAL_CODE", - "region": "REGION", - "district": "DISTRICT", - "city": "CITY", - "settlement": "CITY", - "street": "STREET", - "street_type": "STREET", - "house_num": "HOUSE", - "corpus": "HOUSE", - "structure": "HOUSE", - "apartment": "APARTMENT", -} - - -def _offsets(text: str, tokens: Sequence[str]) -> tuple[tuple[int, int], ...]: - offsets: list[tuple[int, int]] = [] - cursor = 0 - for token in tokens: - start = text.find(token, cursor) - if start < 0: - raise ValueError(f"token {token!r} cannot be aligned in {text!r}") - end = start + len(token) - offsets.append((start, end)) - cursor = end - return tuple(offsets) - - -def gold_spans(row: dict[str, Any]) -> list[Span]: - tokens = row["tokens"] - labels = row["labels"] - if len(tokens) != len(labels): - raise ValueError("prepared row has mismatched tokens and labels") - offsets = _offsets(row["raw"], tokens) - spans: list[Span] = [] - active_label: str | None = None - active_start = active_end = 0 - for label, (start, end) in zip(labels, offsets): - if label != active_label: - if active_label in SCORED_LABELS: - spans.append(Span(active_label, active_start, active_end)) - active_label = label if label in SCORED_LABELS else None - active_start = start - if active_label is not None: - active_end = end - if active_label in SCORED_LABELS: - spans.append(Span(active_label, active_start, active_end)) - return spans - - -def predicted_spans(result: ParsedAddress) -> list[Span]: - grouped: dict[str, list[tuple[int, int]]] = {} - for field, label in PREDICTED_FIELDS.items(): - part = getattr(result, field) - if part is not None: - grouped.setdefault(label, []).append((part.start, part.end)) - return [ - Span( - label, - min(start for start, _ in field_offsets), - max(end for _, end in field_offsets), - ) - for label, field_offsets in sorted(grouped.items()) - ] - - -def _overlap(left: Span, right: Span) -> int: - return max(0, min(left.end, right.end) - max(left.start, right.start)) - - -def _match( - gold: Sequence[Span], - predicted: Sequence[Span], -) -> tuple[list[tuple[int, int]], set[int], set[int]]: - candidates = sorted( - ( - (_overlap(wanted, actual), gold_index, predicted_index) - for gold_index, wanted in enumerate(gold) - for predicted_index, actual in enumerate(predicted) - if wanted.label == actual.label and _overlap(wanted, actual) - ), - reverse=True, - ) - matched_gold: set[int] = set() - matched_predicted: set[int] = set() - matches: list[tuple[int, int]] = [] - for _, gold_index, predicted_index in candidates: - if gold_index in matched_gold or predicted_index in matched_predicted: - continue - matched_gold.add(gold_index) - matched_predicted.add(predicted_index) - matches.append((gold_index, predicted_index)) - return matches, matched_gold, matched_predicted - - -def _metrics(counts: dict[str, int]) -> dict[str, int | float]: - precision = ( - counts["tp"] / (counts["tp"] + counts["fp"]) - if counts["tp"] + counts["fp"] - else 0.0 - ) - recall = ( - counts["tp"] / (counts["tp"] + counts["fn"]) - if counts["tp"] + counts["fn"] - else 0.0 - ) - f1 = ( - 2 * precision * recall / (precision + recall) - if precision + recall - else 0.0 - ) - return { - **counts, - "precision": round(precision, 6), - "recall": round(recall, 6), - "f1": round(f1, 6), - } - - -def _score(rows: Iterable[dict[str, Any]]) -> dict[str, Any]: - counts = { - label: {"tp": 0, "fp": 0, "fn": 0, "support": 0} - for label in SCORED_LABELS - } - token_counts = { - label: {"tp": 0, "fp": 0, "fn": 0, "support": 0} - for label in SCORED_LABELS - } - character_counts = { - label: {"tp": 0, "fp": 0, "fn": 0, "support": 0} - for label in SCORED_LABELS - } - failures: list[dict[str, Any]] = [] - row_count = exact_rows = exact_token_rows = exact_spans = overlap_spans = 0 - tiers: Counter[str] = Counter() - started = time.monotonic() - for row in rows: - row_count += 1 - tiers[row["tier"]] += 1 - gold = gold_spans(row) - predicted = predicted_spans(parse(row["raw"])) - matches, matched_gold, matched_predicted = _match(gold, predicted) - overlap_spans += len(matches) - exact_spans += sum( - gold[gold_index] == predicted[predicted_index] - for gold_index, predicted_index in matches - ) - exact_row = ( - len(matches) == len(gold) == len(predicted) - and all( - gold[gold_index] == predicted[predicted_index] - for gold_index, predicted_index in matches - ) - ) - if exact_row: - exact_rows += 1 - - token_offsets = _offsets(row["raw"], row["tokens"]) - actual_token_labels: list[str] = [] - for start, end in token_offsets: - token_span = Span("", start, end) - candidates = [ - (_overlap(token_span, span), span.label) - for span in predicted - if _overlap(token_span, span) - ] - actual_token_labels.append( - max(candidates)[1] if candidates else "O" - ) - wanted_token_labels = [ - label if label in SCORED_LABELS else "O" - for label in row["labels"] - ] - if actual_token_labels == wanted_token_labels: - exact_token_rows += 1 - for wanted, actual in zip(wanted_token_labels, actual_token_labels): - if wanted in SCORED_LABELS: - token_counts[wanted]["support"] += 1 - if wanted == actual: - if wanted in SCORED_LABELS: - token_counts[wanted]["tp"] += 1 - continue - if actual in SCORED_LABELS: - token_counts[actual]["fp"] += 1 - if wanted in SCORED_LABELS: - token_counts[wanted]["fn"] += 1 - - for label in SCORED_LABELS: - gold_indices = { - index for index, span in enumerate(gold) if span.label == label - } - predicted_indices = { - index - for index, span in enumerate(predicted) - if span.label == label - } - label_matches = sum( - gold_index in gold_indices - and predicted_index in predicted_indices - for gold_index, predicted_index in matches - ) - counts[label]["tp"] += label_matches - counts[label]["fp"] += len(predicted_indices - matched_predicted) - counts[label]["fn"] += len(gold_indices - matched_gold) - counts[label]["support"] += len(gold_indices) - gold_characters = sum( - gold[index].end - gold[index].start for index in gold_indices - ) - predicted_characters = sum( - predicted[index].end - predicted[index].start - for index in predicted_indices - ) - overlapping_characters = sum( - _overlap(gold[gold_index], predicted[predicted_index]) - for gold_index, predicted_index in matches - if gold_index in gold_indices - and predicted_index in predicted_indices - ) - character_counts[label]["tp"] += overlapping_characters - character_counts[label]["fp"] += ( - predicted_characters - overlapping_characters - ) - character_counts[label]["fn"] += ( - gold_characters - overlapping_characters - ) - character_counts[label]["support"] += gold_characters - - if not exact_row and len(failures) < 50: - failures.append( - { - "source_row": row["source_row"], - "example_id": row["example_id"], - "tier": row["tier"], - "raw": row["raw"], - "gold": [asdict(span) for span in gold], - "predicted": [asdict(span) for span in predicted], - } - ) - - fields = {label: _metrics(values) for label, values in counts.items()} - token_fields = { - label: _metrics(values) for label, values in token_counts.items() - } - character_fields = { - label: _metrics(values) for label, values in character_counts.items() - } - totals = { - key: sum(values[key] for values in counts.values()) - for key in ("tp", "fp", "fn", "support") - } - token_totals = { - key: sum(values[key] for values in token_counts.values()) - for key in ("tp", "fp", "fn", "support") - } - character_totals = { - key: sum(values[key] for values in character_counts.values()) - for key in ("tp", "fp", "fn", "support") - } - elapsed = time.monotonic() - started - span_overlap_micro = _metrics(totals) - character_overlap_micro = _metrics(character_totals) - token_label_micro = _metrics(token_totals) - return { - "rows": row_count, - "tiers": dict(sorted(tiers.items())), - "matching": "one-to-one same-label character-span overlap", - "metric_definitions": { - "span_overlap_micro": ( - "binary micro precision, recall, and F1 for one-to-one " - "same-label spans with any character overlap" - ), - "character_overlap_micro": ( - "micro precision, recall, and F1 over same-label overlapping " - "characters" - ), - "token_label_micro": ( - "micro precision, recall, and F1 over aligned source-token " - "labels" - ), - "exact_address_rate": ( - "fraction of rows with identical labels and exact span " - "boundaries" - ), - "exact_token_sequence_rate": ( - "fraction of rows whose complete aligned token-label sequence " - "matches" - ), - "fields": "per-field binary span-overlap metrics", - "character_fields": "per-field character-overlap metrics", - "token_fields": "per-field aligned token-label metrics", - }, - "span_overlap_micro": span_overlap_micro, - "character_overlap_micro": character_overlap_micro, - "token_label_micro": token_label_micro, - # Retained for compatibility with the first published report. - "micro": span_overlap_micro, - "character_micro": character_overlap_micro, - "token_micro": token_label_micro, - "macro_field_f1": round( - sum(float(value["f1"]) for value in fields.values()) - / len(fields), - 6, - ), - "token_macro_field_f1": round( - sum(float(value["f1"]) for value in token_fields.values()) - / len(token_fields), - 6, - ), - "character_macro_field_f1": round( - sum(float(value["f1"]) for value in character_fields.values()) - / len(character_fields), - 6, - ), - "exact_address_rate": round( - exact_rows / row_count if row_count else 0.0, - 6, - ), - "exact_token_sequence_rate": round( - exact_token_rows / row_count if row_count else 0.0, - 6, - ), - "exact_span_recall": round( - exact_spans / totals["support"] if totals["support"] else 0.0, - 6, - ), - "overlap_spans": overlap_spans, - "exact_spans": exact_spans, - "fields": fields, - "character_fields": character_fields, - "token_fields": token_fields, - "elapsed_seconds": round(elapsed, 3), - "rows_per_second": round(row_count / elapsed, 1) if elapsed else None, - "failure_sample": failures, - } - - -def load_rows( - path: Path, - *, - limit: int | None = None, - tier: str | None = None, -) -> Iterable[dict[str, Any]]: - with gzip.open(path, "rt", encoding="utf-8") as source: - yielded = 0 - for line in source: - row = json.loads(line) - if row.get("split") != "test": - raise ValueError("prepared benchmark contains a non-test row") - if tier is not None and row.get("tier") != tier: - continue - yield row - yielded += 1 - if limit is not None and yielded >= limit: - break - - -def _summary(report: dict[str, Any]) -> dict[str, Any]: - return { - key: value - for key, value in report.items() - if key - not in { - "failure_sample", - "fields", - "character_fields", - "token_fields", - "metric_definitions", - "span_overlap_micro", - "character_overlap_micro", - "token_label_micro", - } - } - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--data", type=Path, default=DEFAULT_SAMPLE) - parser.add_argument("--limit", type=int) - parser.add_argument("--output", type=Path) - args = parser.parse_args(argv) - if not args.data.exists(): - parser.error( - f"{args.data} does not exist; run prepare_deepparse.py first" - ) - if args.limit is not None and args.limit <= 0: - parser.error("--limit must be positive") - - score = _score(load_rows(args.data, limit=args.limit)) - report = { - "scope": ( - "untuned external evaluation on clean Russian open-geographic " - "address strings from the sealed Deepparse test split" - ), - "source": { - "repository": "deepparse/worldwide-addresses", - "configuration": "ru", - "license": "CC BY 4.0", - "revision": DATASET_REVISION, - }, - "limitations": [ - "clean registry-derived strings are easier than user-entered text", - "Country and Suburb source tags are retained as context but unscored", - "span overlap gives partial credit and is not exact-value accuracy", - ], - **score, - "slices": { - tier: _summary( - _score(load_rows(args.data, limit=args.limit, tier=tier)) - ) - for tier in ( - "street_house_unit", - "street_house", - "street_only", - "number_or_unit_only", - ) - }, - } - rendered = f"{json.dumps(report, ensure_ascii=False, indent=2)}\n" - if args.output: - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(rendered, encoding="utf-8") - print(rendered, end="") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/evaluation/evaluate_detection.py b/evaluation/evaluate_detection.py deleted file mode 100644 index 4c3ab31..0000000 --- a/evaluation/evaluate_detection.py +++ /dev/null @@ -1,278 +0,0 @@ -#!/usr/bin/env python3 -"""Evaluate free-form message address detection on a transparent fixture.""" - -from __future__ import annotations - -import argparse -import json -from pathlib import Path -import sys -from typing import Any, Callable, Iterable, Sequence - - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT / "src")) - -from address_normalizer import detect_addresses -from address_normalizer.types import DetectedAddress - - -Span = tuple[int, int] - - -def _ratio(numerator: int, denominator: int) -> float: - return numerator / denominator if denominator else 0.0 - - -def _prf(tp: int, fp: int, fn: int) -> dict[str, int | float]: - precision = _ratio(tp, tp + fp) - recall = _ratio(tp, tp + fn) - f1 = _ratio(2 * precision * recall, precision + recall) - return { - "tp": tp, - "fp": fp, - "fn": fn, - "precision": round(precision, 6), - "recall": round(recall, 6), - "f1": round(f1, 6), - } - - -def load_rows(path: Path) -> list[dict[str, Any]]: - """Load and validate the annotated JSONL fixture.""" - - rows: list[dict[str, Any]] = [] - for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): - if not line.strip(): - continue - row = json.loads(line) - if not isinstance(row, dict) or not isinstance(row.get("message"), str): - raise ValueError(f"{path}:{line_number}: message must be a string") - expected = row.get("expected") - if not isinstance(expected, list) or not all( - isinstance(value, str) for value in expected - ): - raise ValueError(f"{path}:{line_number}: expected must be strings") - rows.append(row) - if not rows: - raise ValueError(f"{path}: no rows") - return rows - - -def _expected_spans(message: str, expected: Sequence[str]) -> list[Span]: - spans: list[Span] = [] - search_start = 0 - for value in expected: - start = message.find(value, search_start) - if start < 0: - raise ValueError(f"expected text is absent from message: {value!r}") - spans.append((start, start + len(value))) - search_start = start + len(value) - return spans - - -def _overlap(left: Span, right: Span) -> int: - return max(0, min(left[1], right[1]) - max(left[0], right[0])) - - -def _match( - expected: Sequence[Span], - actual: Sequence[Span], - *, - exact: bool, -) -> tuple[list[tuple[int, int]], set[int], set[int]]: - candidates = sorted( - ( - ( - _overlap(wanted, observed), - expected_index, - actual_index, - ) - for expected_index, wanted in enumerate(expected) - for actual_index, observed in enumerate(actual) - if (wanted == observed if exact else _overlap(wanted, observed) > 0) - ), - reverse=True, - ) - matched_expected: set[int] = set() - matched_actual: set[int] = set() - matches: list[tuple[int, int]] = [] - for _, expected_index, actual_index in candidates: - if ( - expected_index in matched_expected - or actual_index in matched_actual - ): - continue - matched_expected.add(expected_index) - matched_actual.add(actual_index) - matches.append((expected_index, actual_index)) - return matches, matched_expected, matched_actual - - -def _failure_reasons( - expected: Sequence[Span], - actual: Sequence[Span], -) -> list[str]: - _, matched_expected, matched_actual = _match(expected, actual, exact=False) - reasons: set[str] = set() - for expected_index, wanted in enumerate(expected): - if expected_index not in matched_expected: - reasons.add("missed_address") - continue - overlaps = [ - observed for observed in actual if _overlap(wanted, observed) - ] - for observed in overlaps: - if observed[0] < wanted[0] or observed[1] > wanted[1]: - reasons.add("span_includes_context") - if observed[0] > wanted[0] or observed[1] < wanted[1]: - reasons.add("span_drops_address_text") - if len(matched_actual) != len(actual): - reasons.add("spurious_address") - return sorted(reasons) - - -def evaluate( - rows: Iterable[dict[str, Any]], - detector: Callable[[str], Sequence[DetectedAddress]] = detect_addresses, -) -> dict[str, Any]: - """Score exact and overlap spans without treating overlap as exact.""" - - row_count = positive_rows = negative_rows = exact_messages = 0 - correctly_empty = 0 - exact_tp = exact_fp = exact_fn = 0 - overlap_tp = overlap_fp = overlap_fn = 0 - failures: list[dict[str, Any]] = [] - scenario_counts: dict[str, dict[str, int]] = {} - - for row in rows: - row_count += 1 - message = str(row["message"]) - expected = _expected_spans(message, row["expected"]) - detected = tuple(detector(message)) - actual = [item.span for item in detected] - if expected: - positive_rows += 1 - else: - negative_rows += 1 - if not actual: - correctly_empty += 1 - - exact_matches, _, _ = _match(expected, actual, exact=True) - overlap_matches, _, _ = _match(expected, actual, exact=False) - exact_tp += len(exact_matches) - exact_fp += len(actual) - len(exact_matches) - exact_fn += len(expected) - len(exact_matches) - overlap_tp += len(overlap_matches) - overlap_fp += len(actual) - len(overlap_matches) - overlap_fn += len(expected) - len(overlap_matches) - message_exact = expected == actual - exact_messages += int(message_exact) - - scenario = str(row.get("scenario_family", "unspecified")) - scenario_result = scenario_counts.setdefault( - scenario, - {"rows": 0, "exact_messages": 0}, - ) - scenario_result["rows"] += 1 - scenario_result["exact_messages"] += int(message_exact) - - if not message_exact: - failures.append( - { - "id": row.get("id"), - "message": message, - "scenario_family": scenario, - "context_style": row.get("context_style"), - "address_style": row.get("address_style"), - "boundary_style": row.get("boundary_style"), - "ambiguity": row.get("ambiguity"), - "expected": [ - {"text": message[start:end], "span": [start, end]} - for start, end in expected - ], - "actual": [ - { - "text": item.text, - "span": [item.start, item.end], - "confidence": round(item.confidence, 4), - "signals": list(item.signals), - } - for item in detected - ], - "failure_reasons": _failure_reasons(expected, actual), - "notes": row.get("notes"), - } - ) - - scenario_report = { - name: { - **values, - "exact_message_rate": round( - _ratio(values["exact_messages"], values["rows"]), - 6, - ), - } - for name, values in sorted(scenario_counts.items()) - } - return { - "rows": row_count, - "positive_rows": positive_rows, - "negative_rows": negative_rows, - "scope": ( - "small, manually authored detection-behavior fixture; suitable for " - "regression, not a production accuracy claim" - ), - "metric_definitions": { - "exact_span_micro": ( - "one-to-one precision, recall, and F1 requiring exact message " - "boundaries" - ), - "overlap_span_micro": ( - "one-to-one precision, recall, and F1 requiring any character " - "overlap; reported separately because it is lenient" - ), - "exact_message_rate": ( - "fraction of messages where the complete ordered span list is " - "exact" - ), - "negative_message_specificity": ( - "fraction of annotated negative messages returning no spans" - ), - }, - "exact_span_micro": _prf(exact_tp, exact_fp, exact_fn), - "overlap_span_micro": _prf(overlap_tp, overlap_fp, overlap_fn), - "exact_message_rate": round(_ratio(exact_messages, row_count), 6), - "negative_message_specificity": round( - _ratio(correctly_empty, negative_rows), - 6, - ), - "scenarios": scenario_report, - "failure_sample": failures, - } - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument( - "--data", - type=Path, - default=ROOT / "evaluation/detection_reference.jsonl", - ) - parser.add_argument( - "--output", - type=Path, - default=ROOT / "evaluation/detection_report.json", - ) - args = parser.parse_args(argv) - - report = evaluate(load_rows(args.data)) - rendered = json.dumps(report, ensure_ascii=False, indent=2) - print(rendered) - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(f"{rendered}\n", encoding="utf-8") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/evaluation/evaluate_redmadrobot.py b/evaluation/evaluate_redmadrobot.py deleted file mode 100644 index ddcf3d4..0000000 --- a/evaluation/evaluate_redmadrobot.py +++ /dev/null @@ -1,456 +0,0 @@ -"""Evaluate address extraction on an independent Russian NER benchmark.""" - -from __future__ import annotations - -import argparse -import csv -from dataclasses import asdict, dataclass -import hashlib -import json -from pathlib import Path -import sys -from typing import Any, Callable, Iterable, Sequence -from urllib.request import Request, urlopen - - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT / "src")) - -from address_normalizer import parse -from address_normalizer.types import ParsedAddress - - -DATASET_REVISION = "f77ea831274daf980cc45c61a93c226be9d978d6" -DATASET_SHA256 = "6bf544a380a3ee5bec94b946124bea3afaecce49e734679ad0f0c0e7c12977bb" -DATASET_URL = ( - "https://huggingface.co/datasets/redmadrobot-rnd/pii_benchmark/resolve/" - f"{DATASET_REVISION}/test.csv" -) -DEFAULT_DATA = ( - ROOT - / ".cache" - / f"redmadrobot-pii-benchmark-{DATASET_REVISION[:8]}.csv" -) -LOCATION_LABELS = { - "COUNTRY", - "REGION", - "DISTRICT", - "CITY", - "STREET", - "HOUSE", -} -SCORED_LABELS = ("REGION", "DISTRICT", "CITY", "STREET", "HOUSE") -PART_LABELS = { - "region": "REGION", - "district": "DISTRICT", - "city": "CITY", - "settlement": "CITY", - "street": "STREET", - "street_type": "STREET", - "house_num": "HOUSE", - "corpus": "HOUSE", - "structure": "HOUSE", - "apartment": "HOUSE", -} - - -@dataclass(frozen=True, slots=True) -class Span: - label: str - start: int - end: int - - -@dataclass(frozen=True, slots=True) -class AddressSnippet: - source_row: int - text: str - tokens: tuple[str, ...] - tags: tuple[str, ...] - offsets: tuple[tuple[int, int], ...] - - -def _base_label(tag: str) -> str: - return tag[2:] if tag.startswith(("B-", "I-")) else tag - - -def _reconstruct(tokens: Sequence[str]) -> tuple[str, tuple[tuple[int, int], ...]]: - text_parts: list[str] = [] - offsets: list[tuple[int, int]] = [] - cursor = 0 - for index, token in enumerate(tokens): - if index: - text_parts.append(" ") - cursor += 1 - start = cursor - text_parts.append(token) - cursor += len(token) - offsets.append((start, cursor)) - return "".join(text_parts), tuple(offsets) - - -def _clusters(tags: Sequence[str], max_gap: int) -> list[tuple[int, int]]: - location_indices = [ - index - for index, tag in enumerate(tags) - if _base_label(tag) in LOCATION_LABELS - ] - if not location_indices: - return [] - clusters: list[tuple[int, int]] = [] - start = previous = location_indices[0] - for index in location_indices[1:]: - if index - previous - 1 > max_gap: - clusters.append((start, previous + 1)) - start = index - previous = index - clusters.append((start, previous + 1)) - return clusters - - -def load_snippets(path: Path, *, max_gap: int = 3) -> list[AddressSnippet]: - snippets: list[AddressSnippet] = [] - with path.open(encoding="utf-8", newline="") as source: - for row_number, row in enumerate(csv.DictReader(source)): - tokens = tuple(json.loads(row["tokens"])) - tags = tuple(json.loads(row["ner_tags"])) - if len(tokens) != len(tags): - raise ValueError(f"row {row_number}: token/tag length mismatch") - for start, end in _clusters(tags, max_gap): - selected_tags = tags[start:end] - if not any( - _base_label(tag) in SCORED_LABELS - for tag in selected_tags - ): - continue - selected_tokens = tokens[start:end] - text, offsets = _reconstruct(selected_tokens) - snippets.append( - AddressSnippet( - source_row=row_number, - text=text, - tokens=selected_tokens, - tags=selected_tags, - offsets=offsets, - ) - ) - return snippets - - -def _gold_spans(snippet: AddressSnippet) -> list[Span]: - spans: list[Span] = [] - active_label: str | None = None - active_start = 0 - active_end = 0 - for tag, (start, end) in zip(snippet.tags, snippet.offsets): - label = _base_label(tag) - continues = tag.startswith("I-") and label == active_label - if active_label is not None and not continues: - if active_label in SCORED_LABELS: - spans.append(Span(active_label, active_start, active_end)) - active_label = None - if label not in LOCATION_LABELS: - continue - if active_label is None: - active_label = label - active_start = start - active_end = end - if active_label in SCORED_LABELS: - spans.append(Span(active_label, active_start, active_end)) - return spans - - -def _predicted_spans(result: ParsedAddress) -> list[Span]: - grouped: dict[str, list[tuple[int, int]]] = {} - for field, label in PART_LABELS.items(): - part = getattr(result, field) - if part is not None: - grouped.setdefault(label, []).append((part.start, part.end)) - return [ - Span( - label=label, - start=min(start for start, _ in offsets), - end=max(end for _, end in offsets), - ) - for label, offsets in sorted(grouped.items()) - ] - - -def _overlap(left: Span, right: Span) -> int: - return max(0, min(left.end, right.end) - max(left.start, right.start)) - - -def _match( - gold: Sequence[Span], - predicted: Sequence[Span], -) -> tuple[list[tuple[int, int]], set[int], set[int]]: - candidates = sorted( - ( - (_overlap(wanted, actual), gold_index, predicted_index) - for gold_index, wanted in enumerate(gold) - for predicted_index, actual in enumerate(predicted) - if wanted.label == actual.label and _overlap(wanted, actual) - ), - reverse=True, - ) - matched_gold: set[int] = set() - matched_predicted: set[int] = set() - matches: list[tuple[int, int]] = [] - for _, gold_index, predicted_index in candidates: - if gold_index in matched_gold or predicted_index in matched_predicted: - continue - matched_gold.add(gold_index) - matched_predicted.add(predicted_index) - matches.append((gold_index, predicted_index)) - return matches, matched_gold, matched_predicted - - -def _prf(values: dict[str, int]) -> dict[str, int | float]: - precision = ( - values["tp"] / (values["tp"] + values["fp"]) - if values["tp"] + values["fp"] - else 0.0 - ) - recall = ( - values["tp"] / (values["tp"] + values["fn"]) - if values["tp"] + values["fn"] - else 0.0 - ) - f1 = ( - 2 * precision * recall / (precision + recall) - if precision + recall - else 0.0 - ) - return { - **values, - "precision": round(precision, 6), - "recall": round(recall, 6), - "f1": round(f1, 6), - } - - -def evaluate( - snippets: Iterable[AddressSnippet], - parse_address: Callable[[str], ParsedAddress] = parse, -) -> dict[str, Any]: - counts = { - label: {"tp": 0, "fp": 0, "fn": 0, "support": 0} - for label in SCORED_LABELS - } - failures: list[dict[str, Any]] = [] - snippet_count = exact_span_matches = overlap_matches = 0 - source_rows: set[int] = set() - - for snippet in snippets: - snippet_count += 1 - source_rows.add(snippet.source_row) - gold = _gold_spans(snippet) - predicted = _predicted_spans(parse_address(snippet.text)) - matches, matched_gold, matched_predicted = _match(gold, predicted) - overlap_matches += len(matches) - exact_span_matches += sum( - gold[gold_index] == predicted[predicted_index] - for gold_index, predicted_index in matches - ) - - for label in SCORED_LABELS: - gold_indices = { - index for index, span in enumerate(gold) if span.label == label - } - predicted_indices = { - index - for index, span in enumerate(predicted) - if span.label == label - } - label_matches = sum( - gold_index in gold_indices - and predicted_index in predicted_indices - for gold_index, predicted_index in matches - ) - counts[label]["tp"] += label_matches - counts[label]["fp"] += len(predicted_indices - matched_predicted) - counts[label]["fn"] += len(gold_indices - matched_gold) - counts[label]["support"] += len(gold_indices) - - if ( - len(matched_gold) != len(gold) - or len(matched_predicted) != len(predicted) - ) and len(failures) < 50: - failures.append( - { - "source_row": snippet.source_row, - "text": snippet.text, - "gold": [asdict(span) for span in gold], - "predicted": [asdict(span) for span in predicted], - } - ) - - fields = {label: _prf(values) for label, values in counts.items()} - totals = { - name: sum(values[name] for values in counts.values()) - for name in ("tp", "fp", "fn", "support") - } - supported_f1 = [ - float(values["f1"]) - for values in fields.values() - if values["support"] - ] - span_overlap_micro = _prf(totals) - return { - "source_rows": len(source_rows), - "address_snippets": snippet_count, - "matching": ( - "one-to-one same-label span overlap; address windows are oracle-" - "cropped from the benchmark's BIO annotations" - ), - "metric_definitions": { - "span_overlap_micro": ( - "micro precision, recall, and F1 for one-to-one same-label " - "spans with any character overlap" - ), - "exact_span_recall": ( - "exact-boundary same-label matches divided by gold span count" - ), - "fields": "per-field span-overlap precision, recall, and F1", - }, - "span_overlap_micro": span_overlap_micro, - # Retained for compatibility with the first published report. - "micro": span_overlap_micro, - "macro_field_f1": round( - sum(supported_f1) / len(supported_f1) if supported_f1 else 0.0, - 6, - ), - "overlap_matches": overlap_matches, - "exact_span_matches": exact_span_matches, - "exact_span_recall": round( - exact_span_matches / totals["support"] if totals["support"] else 0.0, - 6, - ), - "fields": fields, - "failure_sample": failures, - } - - -def _without_failures(report: dict[str, Any]) -> dict[str, Any]: - return { - key: value - for key, value in report.items() - if key - not in { - "failure_sample", - "metric_definitions", - "span_overlap_micro", - } - } - - -def _sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as source: - for chunk in iter(lambda: source.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def download_dataset(path: Path) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - temporary = path.with_suffix(f"{path.suffix}.part") - request = Request(DATASET_URL, headers={"User-Agent": "address-normalizer/2"}) - with urlopen(request, timeout=60) as response, temporary.open("wb") as output: - while chunk := response.read(1024 * 1024): - output.write(chunk) - actual_sha256 = _sha256(temporary) - if actual_sha256 != DATASET_SHA256: - temporary.unlink(missing_ok=True) - raise ValueError( - "external benchmark checksum mismatch: " - f"expected {DATASET_SHA256}, got {actual_sha256}" - ) - temporary.replace(path) - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--data", type=Path, default=DEFAULT_DATA) - parser.add_argument( - "--download", - action="store_true", - help="download the pinned benchmark revision before evaluation", - ) - parser.add_argument("--max-gap", type=int, default=3) - parser.add_argument("--output", type=Path) - args = parser.parse_args(argv) - - if args.download: - download_dataset(args.data) - if not args.data.exists(): - parser.error( - f"{args.data} does not exist; provide --data or run with --download" - ) - actual_sha256 = _sha256(args.data) - if actual_sha256 != DATASET_SHA256: - parser.error( - "benchmark checksum mismatch: " - f"expected {DATASET_SHA256}, got {actual_sha256}" - ) - if args.max_gap < 0: - parser.error("--max-gap must be non-negative") - - snippets = load_snippets(args.data, max_gap=args.max_gap) - multi_field = [ - snippet - for snippet in snippets - if len({span.label for span in _gold_spans(snippet)}) >= 2 - ] - street_and_house = [ - snippet - for snippet in snippets - if {"STREET", "HOUSE"} - <= {span.label for span in _gold_spans(snippet)} - ] - administrative_only = [ - snippet - for snippet in snippets - if {span.label for span in _gold_spans(snippet)} - <= {"REGION", "DISTRICT", "CITY"} - ] - report = { - "scope": ( - "independent, untuned external evaluation on address snippets from " - "the RedMadRobot Russian PII NER benchmark" - ), - "source": { - "repository": "redmadrobot-rnd/pii_benchmark", - "license": "MIT", - "revision": DATASET_REVISION, - "sha256": actual_sha256, - "url": DATASET_URL, - "limitations": ( - "production-log-shaped and manually annotated, with real " - "personal values replaced; includes synthetic document-style " - "examples and hard negatives" - ), - }, - "windowing": { - "max_non_location_tokens_between_spans": args.max_gap, - "country_is_context_only": True, - }, - **evaluate(snippets), - "slices": { - "multi_field": _without_failures(evaluate(multi_field)), - "street_and_house": _without_failures(evaluate(street_and_house)), - "administrative_only": _without_failures( - evaluate(administrative_only) - ), - }, - } - rendered = f"{json.dumps(report, ensure_ascii=False, indent=2)}\n" - if args.output: - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(rendered, encoding="utf-8") - print(rendered, end="") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/evaluation/evaluate_redmadrobot_detection.py b/evaluation/evaluate_redmadrobot_detection.py deleted file mode 100644 index cb19670..0000000 --- a/evaluation/evaluate_redmadrobot_detection.py +++ /dev/null @@ -1,318 +0,0 @@ -#!/usr/bin/env python3 -"""Evaluate message-level address detection on complete RedMadRobot rows.""" - -from __future__ import annotations - -import argparse -from collections import Counter -import csv -from dataclasses import asdict, dataclass -import json -from pathlib import Path -import sys -from typing import Any, Callable, Iterable, Sequence - - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT / "src")) - -from address_normalizer import detect_addresses -from address_normalizer.types import DetectedAddress - -from evaluate_redmadrobot import ( - DATASET_REVISION, - DATASET_SHA256, - _base_label, - _clusters, - _reconstruct, - _sha256, -) - - -DEFAULT_DATA = ( - ROOT - / ".cache" - / "external" - / f"redmadrobot-pii-benchmark-{DATASET_REVISION[:8]}.csv" -) - - -@dataclass(frozen=True, slots=True) -class Span: - start: int - end: int - - -@dataclass(frozen=True, slots=True) -class Message: - source_row: int - text: str - gold: tuple[Span, ...] - - -def _ratio(numerator: int, denominator: int) -> float: - return numerator / denominator if denominator else 0.0 - - -def _prf(tp: int, fp: int, fn: int) -> dict[str, int | float]: - precision = _ratio(tp, tp + fp) - recall = _ratio(tp, tp + fn) - f1 = _ratio(2 * precision * recall, precision + recall) - return { - "tp": tp, - "fp": fp, - "fn": fn, - "precision": round(precision, 6), - "recall": round(recall, 6), - "f1": round(f1, 6), - } - - -def load_messages(path: Path, *, max_gap: int = 3) -> list[Message]: - """Reconstruct complete messages and STREET+HOUSE gold windows.""" - - messages: list[Message] = [] - with path.open(encoding="utf-8", newline="") as source: - for row_number, row in enumerate(csv.DictReader(source)): - tokens = tuple(json.loads(row["tokens"])) - tags = tuple(json.loads(row["ner_tags"])) - if len(tokens) != len(tags): - raise ValueError(f"row {row_number}: token/tag length mismatch") - text, offsets = _reconstruct(tokens) - spans: list[Span] = [] - for start, end in _clusters(tags, max_gap): - labels = { - _base_label(tag) - for tag in tags[start:end] - } - if not {"STREET", "HOUSE"} <= labels: - continue - spans.append( - Span( - start=offsets[start][0], - end=offsets[end - 1][1], - ) - ) - messages.append( - Message( - source_row=row_number, - text=text, - gold=tuple(spans), - ) - ) - return messages - - -def _overlap(left: Span, right: Span) -> int: - return max(0, min(left.end, right.end) - max(left.start, right.start)) - - -def _match( - expected: Sequence[Span], - actual: Sequence[Span], - *, - exact: bool, -) -> tuple[list[tuple[int, int]], set[int], set[int]]: - candidates = sorted( - ( - ( - _overlap(wanted, observed), - expected_index, - actual_index, - ) - for expected_index, wanted in enumerate(expected) - for actual_index, observed in enumerate(actual) - if ( - wanted == observed - if exact - else _overlap(wanted, observed) > 0 - ) - ), - reverse=True, - ) - matched_expected: set[int] = set() - matched_actual: set[int] = set() - matches: list[tuple[int, int]] = [] - for _, expected_index, actual_index in candidates: - if ( - expected_index in matched_expected - or actual_index in matched_actual - ): - continue - matched_expected.add(expected_index) - matched_actual.add(actual_index) - matches.append((expected_index, actual_index)) - return matches, matched_expected, matched_actual - - -def _failure_reasons( - expected: Sequence[Span], - actual: Sequence[Span], -) -> list[str]: - matches, matched_expected, matched_actual = _match( - expected, - actual, - exact=False, - ) - reasons: set[str] = set() - for expected_index, actual_index in matches: - wanted = expected[expected_index] - observed = actual[actual_index] - if observed.start < wanted.start or observed.end > wanted.end: - reasons.add("span_includes_context") - if observed.start > wanted.start or observed.end < wanted.end: - reasons.add("span_drops_gold_text") - if len(matched_expected) != len(expected): - reasons.add("missed_address") - if len(matched_actual) != len(actual): - reasons.add("spurious_address") - return sorted(reasons) - - -def evaluate( - messages: Iterable[Message], - detector: Callable[[str], Sequence[DetectedAddress]] = detect_addresses, -) -> dict[str, Any]: - """Score complete messages without oracle-cropping detector input.""" - - rows = positive_rows = negative_rows = exact_messages = correctly_empty = 0 - exact_tp = exact_fp = exact_fn = 0 - overlap_tp = overlap_fp = overlap_fn = 0 - failures: list[dict[str, Any]] = [] - failure_reasons: Counter[str] = Counter() - - for message in messages: - rows += 1 - if message.gold: - positive_rows += 1 - else: - negative_rows += 1 - detected = tuple(detector(message.text)) - actual = tuple(Span(item.start, item.end) for item in detected) - if not message.gold and not actual: - correctly_empty += 1 - - exact_matches, _, _ = _match(message.gold, actual, exact=True) - overlap_matches, _, _ = _match(message.gold, actual, exact=False) - exact_tp += len(exact_matches) - exact_fp += len(actual) - len(exact_matches) - exact_fn += len(message.gold) - len(exact_matches) - overlap_tp += len(overlap_matches) - overlap_fp += len(actual) - len(overlap_matches) - overlap_fn += len(message.gold) - len(overlap_matches) - - message_exact = list(message.gold) == list(actual) - exact_messages += int(message_exact) - if not message_exact: - reasons = _failure_reasons(message.gold, actual) - failure_reasons.update(reasons) - failures.append( - { - "source_row": message.source_row, - "text": message.text, - "gold": [ - { - **asdict(span), - "text": message.text[span.start : span.end], - } - for span in message.gold - ], - "predicted": [ - { - "start": item.start, - "end": item.end, - "text": item.text, - "confidence": round(item.confidence, 4), - "signals": list(item.signals), - } - for item in detected - ], - "failure_reasons": reasons, - } - ) - - return { - "rows": rows, - "positive_rows": positive_rows, - "negative_rows": negative_rows, - "scope": ( - "complete reconstructed benchmark messages; gold address windows " - "must contain both STREET and HOUSE labels" - ), - "limitations": [ - ( - "The source is a PII NER benchmark, not a detector-specific " - "Russian message sample." - ), - ( - "Gold spans follow BIO annotation boundaries while predicted " - "spans intentionally include parseable markers and units." - ), - ( - "Rows without a STREET+HOUSE cluster are treated as detection " - "negatives even when they contain isolated location entities." - ), - ], - "metric_definitions": { - "span_overlap_micro": ( - "one-to-one precision, recall, and F1 for any character " - "overlap between a detected span and a STREET+HOUSE gold window" - ), - "exact_span_micro": ( - "one-to-one precision, recall, and F1 requiring identical " - "half-open boundaries" - ), - "exact_message_rate": ( - "fraction of complete messages whose ordered span lists match" - ), - "negative_message_specificity": ( - "fraction of messages without a STREET+HOUSE gold window where " - "the detector returns no span" - ), - }, - "span_overlap_micro": _prf(overlap_tp, overlap_fp, overlap_fn), - "exact_span_micro": _prf(exact_tp, exact_fp, exact_fn), - "exact_message_rate": round(_ratio(exact_messages, rows), 6), - "negative_message_specificity": round( - _ratio(correctly_empty, negative_rows), - 6, - ), - "failure_case_count": len(failures), - "failure_rows_by_reason": dict(failure_reasons.most_common()), - "failure_cases": failures, - } - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--data", type=Path, default=DEFAULT_DATA) - parser.add_argument("--max-gap", type=int, default=3) - parser.add_argument( - "--output", - type=Path, - default=ROOT / "evaluation/redmadrobot_detection_report.json", - ) - args = parser.parse_args(argv) - if not args.data.exists(): - raise SystemExit( - f"dataset not found: {args.data}; run the RedMadRobot download " - "command documented in evaluation/README.md" - ) - if _sha256(args.data) != DATASET_SHA256: - raise SystemExit("RedMadRobot dataset checksum mismatch") - - report = evaluate(load_messages(args.data, max_gap=args.max_gap)) - report["source"] = { - "dataset": "redmadrobot-rnd/pii_benchmark", - "revision": DATASET_REVISION, - "sha256": DATASET_SHA256, - } - rendered = json.dumps(report, ensure_ascii=False, indent=2) - print(rendered) - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(f"{rendered}\n", encoding="utf-8") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/evaluation/legacy_reference_500_failure_summary.json b/evaluation/legacy_reference_500_failure_summary.json deleted file mode 100644 index 01bf1b0..0000000 --- a/evaluation/legacy_reference_500_failure_summary.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "dataset": "legacy_reference_500", - "rows": 500, - "exact_rows": 402, - "failed_rows": 98, - "diagnostic_semantics": "likely_causes are deterministic triage hypotheses and require human review; they are not causal ground truth", - "failure_rows_by_likely_cause": { - "component_label_confusion": 25, - "numeric_component_not_recognized": 24, - "conflicting_street_markers": 22, - "reference_infers_missing_street_type": 20, - "ambiguous_or_unsupported_abbreviation": 15, - "unmarked_numeric_role_ambiguity": 14, - "compound_or_letter_number_boundary": 14, - "numeric_value_or_role": 13, - "administrative_label_or_boundary": 11, - "normalization_only_difference": 7, - "street_label_or_value": 7, - "street_type_recognition": 7, - "street_span_boundary": 6, - "administrative_component_missed": 5, - "spurious_numeric_component": 5, - "reference_conflicts_with_explicit_numeric_marker": 3, - "spurious_administrative_component": 2, - "reference_conflicts_with_explicit_street_type": 1 - }, - "failure_rows_by_primary_likely_cause": { - "conflicting_street_markers": 22, - "reference_infers_missing_street_type": 20, - "ambiguous_or_unsupported_abbreviation": 15, - "unmarked_numeric_role_ambiguity": 14, - "compound_or_letter_number_boundary": 7, - "administrative_label_or_boundary": 6, - "street_type_recognition": 4, - "reference_conflicts_with_explicit_numeric_marker": 3, - "numeric_component_not_recognized": 3, - "component_label_confusion": 1, - "reference_conflicts_with_explicit_street_type": 1, - "spurious_administrative_component": 1, - "street_span_boundary": 1 - }, - "failure_rows_by_mismatch_field": { - "street_type": 61, - "house_num": 25, - "apartment": 21, - "street": 20, - "structure": 7, - "corpus": 6, - "region": 6, - "settlement": 5, - "city": 5, - "district": 2 - }, - "scenario_tags_on_failure_rows": { - "administrative_expected": 98, - "postal_code": 97, - "street_marker": 68, - "unit_expected": 57, - "house_marker": 56, - "compact_punctuation": 45, - "unit_marker": 42, - "street_marker_absent": 30, - "street_marker_prefix": 30, - "country_phrase": 26, - "street_marker_multiple": 22, - "street_marker_suffix": 15, - "ambiguous_abbreviation": 15, - "compound_number": 15, - "unmarked_numeric_sequence": 14, - "multiword_street": 14, - "letter_suffix_number": 12, - "repeated_city": 10, - "hyphenated_number": 9, - "slash_number": 8, - "ordinal_street": 3, - "street_marker_present_street_not_literal": 1 - }, - "representative_failure_sample": [ - { - "id": "legacy-good-0005", - "raw": "109052, Москва г, ул.Рязанский проспект, д.2", - "mismatch_fields": "street_type", - "primary_likely_cause": "conflicting_street_markers", - "likely_causes": "conflicting_street_markers", - "failure_summary": "street_type: expected='пр-кт', actual='ул'", - "unparsed_spans": "[\"г\",\"проспект\"]" - }, - { - "id": "legacy-good-0359", - "raw": "109651, Российская Федерация, 77 г. Москва, Москва, Батайский проезд, 17, 294", - "mismatch_fields": "house_num|apartment", - "primary_likely_cause": "unmarked_numeric_role_ambiguity", - "likely_causes": "component_label_confusion|numeric_component_not_recognized|numeric_value_or_role|unmarked_numeric_role_ambiguity", - "failure_summary": "house_num: expected='17', actual='294'; apartment: expected='294', actual=None", - "unparsed_spans": "[\"77\",\"Москва\",\"17\"]" - }, - { - "id": "legacy-good-0165", - "raw": "142701, Московская область, Ленинский район, г.Видное, пр.Ленинского Комсомола, д.15, корп.2, офис 108", - "mismatch_fields": "street_type", - "primary_likely_cause": "ambiguous_or_unsupported_abbreviation", - "likely_causes": "ambiguous_or_unsupported_abbreviation", - "failure_summary": "street_type: expected='пр-кт', actual=None", - "unparsed_spans": "[\"пр\"]" - }, - { - "id": "legacy-good-0751", - "raw": "115682,г.Москва, ул.Ореховый бул.,д.26,корп.2", - "mismatch_fields": "street|street_type", - "primary_likely_cause": "conflicting_street_markers", - "likely_causes": "conflicting_street_markers|street_span_boundary", - "failure_summary": "street: expected='Ореховый', actual='Ореховый Бул'; street_type: expected='б-р', actual='ул'", - "unparsed_spans": "[]" - }, - { - "id": "legacy-good-0089", - "raw": "142140, Москва г, Михайлово-Ярцевское п, Исаково д, ул.Исаково-2, д.2", - "mismatch_fields": "district|settlement", - "primary_likely_cause": "component_label_confusion", - "likely_causes": "administrative_component_missed|administrative_label_or_boundary|component_label_confusion", - "failure_summary": "district: expected='Михайлово-Ярцевское', actual=None; settlement: expected='Исаково', actual='Михайлово-Ярцевское'", - "unparsed_spans": "[\"г\",\"Исаково д\"]" - }, - { - "id": "legacy-good-0795", - "raw": "119991, г. Москва, Ордынка Б., д. 25, с.1", - "mismatch_fields": "street|street_type|structure", - "primary_likely_cause": "ambiguous_or_unsupported_abbreviation", - "likely_causes": "ambiguous_or_unsupported_abbreviation|normalization_only_difference|numeric_component_not_recognized", - "failure_summary": "street: expected='Ордынка Б.', actual='Ордынка Б'; street_type: expected='ул', actual=None; structure: expected='1', actual=None", - "unparsed_spans": "[\"с.1\"]" - }, - { - "id": "legacy-good-0006", - "raw": "105066, г. Москва Денисовский переулок, д.9", - "mismatch_fields": "city|street|street_type", - "primary_likely_cause": "administrative_label_or_boundary", - "likely_causes": "administrative_label_or_boundary|street_label_or_value|street_type_recognition", - "failure_summary": "city: expected='Москва', actual='Москва Денисовский'; street: expected='Денисовский', actual='Переулок'; street_type: expected='пер', actual=None", - "unparsed_spans": "[]" - }, - { - "id": "legacy-good-0935", - "raw": "115201, г. Москва, Котляковская, 4", - "mismatch_fields": "street_type", - "primary_likely_cause": "reference_infers_missing_street_type", - "likely_causes": "reference_infers_missing_street_type", - "failure_summary": "street_type: expected='ул', actual=None", - "unparsed_spans": "[]" - }, - { - "id": "legacy-good-0029", - "raw": "117574, г. Москва, ул. Одоевского, д. 3, корп. 7", - "mismatch_fields": "street_type", - "primary_likely_cause": "reference_conflicts_with_explicit_street_type", - "likely_causes": "reference_conflicts_with_explicit_street_type", - "failure_summary": "street_type: expected='пр-д', actual='ул'", - "unparsed_spans": "[]" - }, - { - "id": "legacy-good-0036", - "raw": "140083, Московская область, г. Лыткарино, ул. Парковая, стр. 4А.", - "mismatch_fields": "house_num|structure", - "primary_likely_cause": "reference_conflicts_with_explicit_numeric_marker", - "likely_causes": "component_label_confusion|compound_or_letter_number_boundary|reference_conflicts_with_explicit_numeric_marker|spurious_numeric_component", - "failure_summary": "house_num: expected='4А', actual=None; structure: expected=None, actual='4А'", - "unparsed_spans": "[]" - } - ] -} diff --git a/evaluation/legacy_reference_500_report.json b/evaluation/legacy_reference_500_report.json deleted file mode 100644 index 66aa9ef..0000000 --- a/evaluation/legacy_reference_500_report.json +++ /dev/null @@ -1,958 +0,0 @@ -{ - "rows": 500, - "review_statuses": { - "legacy_reference_not_independently_rereviewed": 500 - }, - "metric_definitions": { - "exact_address_rate": "fraction of rows where every public component value matches", - "no_unparsed_rate": "fraction of rows with no residual word or number spans", - "exact_component_value_micro": "micro precision, recall, and F1 over case-insensitive exact component values after whitespace and ё/е folding" - }, - "exact_address_rate": 0.804, - "no_unparsed_rate": 0.768, - "exact_component_value_micro": { - "tp": 2728, - "fp": 85, - "fn": 151, - "precision": 0.969783, - "recall": 0.947551, - "f1": 0.958538 - }, - "micro": { - "tp": 2728, - "fp": 85, - "fn": 151, - "precision": 0.969783, - "recall": 0.947551, - "f1": 0.958538 - }, - "fields": { - "postal_code": { - "tp": 498, - "fp": 0, - "fn": 0, - "support": 498, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "region": { - "tp": 53, - "fp": 6, - "fn": 5, - "support": 58, - "precision": 0.898305, - "recall": 0.913793, - "f1": 0.905983 - }, - "district": { - "tp": 10, - "fp": 1, - "fn": 1, - "support": 11, - "precision": 0.909091, - "recall": 0.909091, - "f1": 0.909091 - }, - "city": { - "tp": 491, - "fp": 5, - "fn": 5, - "support": 496, - "precision": 0.989919, - "recall": 0.989919, - "f1": 0.989919 - }, - "settlement": { - "tp": 4, - "fp": 1, - "fn": 5, - "support": 9, - "precision": 0.8, - "recall": 0.444444, - "f1": 0.571429 - }, - "street": { - "tp": 480, - "fp": 20, - "fn": 20, - "support": 500, - "precision": 0.96, - "recall": 0.96, - "f1": 0.96 - }, - "street_type": { - "tp": 439, - "fp": 24, - "fn": 61, - "support": 500, - "precision": 0.948164, - "recall": 0.878, - "f1": 0.911734 - }, - "house_num": { - "tp": 475, - "fp": 21, - "fn": 25, - "support": 500, - "precision": 0.957661, - "recall": 0.95, - "f1": 0.953815 - }, - "corpus": { - "tp": 69, - "fp": 0, - "fn": 6, - "support": 75, - "precision": 1.0, - "recall": 0.92, - "f1": 0.958333 - }, - "structure": { - "tp": 133, - "fp": 4, - "fn": 4, - "support": 137, - "precision": 0.970803, - "recall": 0.970803, - "f1": 0.970803 - }, - "apartment": { - "tp": 76, - "fp": 3, - "fn": 19, - "support": 95, - "precision": 0.962025, - "recall": 0.8, - "f1": 0.873563 - } - }, - "failure_sample": [ - { - "id": "legacy-good-0005", - "raw": "109052, Москва г, ул.Рязанский проспект, д.2", - "mismatches": { - "street_type": { - "expected": "пр-кт", - "actual": "ул" - } - }, - "unparsed": [ - "г", - "проспект" - ] - }, - { - "id": "legacy-good-0359", - "raw": "109651, Российская Федерация, 77 г. Москва, Москва, Батайский проезд, 17, 294", - "mismatches": { - "house_num": { - "expected": "17", - "actual": "294" - }, - "apartment": { - "expected": "294", - "actual": null - } - }, - "unparsed": [ - "77", - "Москва", - "17" - ] - }, - { - "id": "legacy-good-0165", - "raw": "142701, Московская область, Ленинский район, г.Видное, пр.Ленинского Комсомола, д.15, корп.2, офис 108", - "mismatches": { - "street_type": { - "expected": "пр-кт", - "actual": null - } - }, - "unparsed": [ - "пр" - ] - }, - { - "id": "legacy-good-0083", - "raw": "111123, г. Москва, Электродный пр., д.1б", - "mismatches": { - "street_type": { - "expected": "пр-д", - "actual": null - } - }, - "unparsed": [ - "пр" - ] - }, - { - "id": "legacy-good-0751", - "raw": "115682,г.Москва, ул.Ореховый бул.,д.26,корп.2", - "mismatches": { - "street": { - "expected": "Ореховый", - "actual": "Ореховый Бул" - }, - "street_type": { - "expected": "б-р", - "actual": "ул" - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0089", - "raw": "142140, Москва г, Михайлово-Ярцевское п, Исаково д, ул.Исаково-2, д.2", - "mismatches": { - "district": { - "expected": "Михайлово-Ярцевское", - "actual": null - }, - "settlement": { - "expected": "Исаково", - "actual": "Михайлово-Ярцевское" - } - }, - "unparsed": [ - "г", - "Исаково д" - ] - }, - { - "id": "legacy-good-0079", - "raw": "119619, г Москва, ул Авиаторов, 9, 2, 50", - "mismatches": { - "house_num": { - "expected": "9", - "actual": "50" - }, - "corpus": { - "expected": "2", - "actual": null - }, - "apartment": { - "expected": "50", - "actual": null - } - }, - "unparsed": [ - "9, 2" - ] - }, - { - "id": "legacy-good-0749", - "raw": "109117, г. Москва, Волгоградский пр.,д.113,к.5", - "mismatches": { - "street_type": { - "expected": "пр-кт", - "actual": null - } - }, - "unparsed": [ - "пр" - ] - }, - { - "id": "legacy-good-0812", - "raw": "125009, Москва г, ул.Брюсов переулок, дом 21, строение 2", - "mismatches": { - "street_type": { - "expected": "пер", - "actual": "ул" - } - }, - "unparsed": [ - "г", - "переулок" - ] - }, - { - "id": "legacy-good-0795", - "raw": "119991, г. Москва, Ордынка Б., д. 25, с.1", - "mismatches": { - "street": { - "expected": "Ордынка Б.", - "actual": "Ордынка Б" - }, - "street_type": { - "expected": "ул", - "actual": null - }, - "structure": { - "expected": "1", - "actual": null - } - }, - "unparsed": [ - "с.1" - ] - }, - { - "id": "legacy-good-0006", - "raw": "105066, г. Москва Денисовский переулок, д.9", - "mismatches": { - "city": { - "expected": "Москва", - "actual": "Москва Денисовский" - }, - "street": { - "expected": "Денисовский", - "actual": "Переулок" - }, - "street_type": { - "expected": "пер", - "actual": null - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0483", - "raw": "428017, Чувашская Республика, г. Чебоксары, ул. Гузовского,11,офис 18", - "mismatches": { - "region": { - "expected": "Чувашская Республика", - "actual": "Чувашская" - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0935", - "raw": "115201, г. Москва, Котляковская, 4", - "mismatches": { - "street_type": { - "expected": "ул", - "actual": null - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0029", - "raw": "117574, г. Москва, ул. Одоевского, д. 3, корп. 7", - "mismatches": { - "street_type": { - "expected": "пр-д", - "actual": "ул" - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0566", - "raw": "142530, Российская Федерация, Московская область, Электрогорск г, Буденного, 1", - "mismatches": { - "street_type": { - "expected": "ул", - "actual": null - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0418", - "raw": "127521, Российская Федерация, Москва, Москва, пр. Марьиной Рощи 17-й, д. 13, стр. 5", - "mismatches": { - "street": { - "expected": "Марьиной Рощи 17-й", - "actual": "Марьиной Рощи" - }, - "street_type": { - "expected": "пр-д", - "actual": null - } - }, - "unparsed": [ - "Москва, пр", - "17-й" - ] - }, - { - "id": "legacy-good-0719", - "raw": "119200, Москва г, ул.Смоленская-Сенная пл., д.32/34", - "mismatches": { - "street_type": { - "expected": "пл", - "actual": "ул" - } - }, - "unparsed": [ - "г", - "пл" - ] - }, - { - "id": "legacy-good-0601", - "raw": "194064, г. Санкт-Петербург, пр. Тихорецкий, д. 21", - "mismatches": { - "street_type": { - "expected": "пр-кт", - "actual": null - } - }, - "unparsed": [ - "пр" - ] - }, - { - "id": "legacy-good-0036", - "raw": "140083, Московская область, г. Лыткарино, ул. Парковая, стр. 4А.", - "mismatches": { - "house_num": { - "expected": "4А", - "actual": null - }, - "structure": { - "expected": null, - "actual": "4А" - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0216", - "raw": "125445, г Москва, ул Смольная, 24А, помещение №8", - "mismatches": { - "house_num": { - "expected": "24А", - "actual": "24" - } - }, - "unparsed": [ - "А" - ] - }, - { - "id": "legacy-good-0095", - "raw": "127055, г Москва, пер Порядковый, 21, 401", - "mismatches": { - "house_num": { - "expected": "21", - "actual": "401" - }, - "apartment": { - "expected": "401", - "actual": null - } - }, - "unparsed": [ - "21" - ] - }, - { - "id": "legacy-good-0868", - "raw": "109144, , Российская Федерация, г.Москва, Новомарьинская, 4", - "mismatches": { - "street_type": { - "expected": "ул", - "actual": null - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0425", - "raw": "143960, Российская Федерация, 50 Московская область, Реутов, Фабричная, д.7", - "mismatches": { - "region": { - "expected": "Московская", - "actual": "50 Московская" - }, - "street_type": { - "expected": "ул", - "actual": null - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0264", - "raw": "117312, Москва, Вавилова, 9", - "mismatches": { - "street_type": { - "expected": "ул", - "actual": null - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0818", - "raw": "127055, Москва г, ул.Порядковый пер, д.21 - 401", - "mismatches": { - "street_type": { - "expected": "пер", - "actual": "ул" - } - }, - "unparsed": [ - "г", - "пер" - ] - }, - { - "id": "legacy-good-0242", - "raw": "142000, обл Московская, г Домодедово, мкр Северный, ш Каширское, 7", - "mismatches": { - "settlement": { - "expected": "Северный", - "actual": null - }, - "street": { - "expected": "Каширское", - "actual": "Северный" - }, - "street_type": { - "expected": "ш", - "actual": "мкр" - } - }, - "unparsed": [ - "ш Каширское" - ] - }, - { - "id": "legacy-good-0563", - "raw": "107076, г. Москва, Варшавское шоссе, д.1, стр.6, ком. 35", - "mismatches": { - "apartment": { - "expected": "35", - "actual": null - } - }, - "unparsed": [ - "ком. 35" - ] - }, - { - "id": "legacy-good-0988", - "raw": "248017, Российская Федерация, Калужская область, Калуга, Московская, 292", - "mismatches": { - "street_type": { - "expected": "ул", - "actual": null - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0065", - "raw": "105066, Москва г, ул.Денисовский переулок, д.9", - "mismatches": { - "street_type": { - "expected": "пер", - "actual": "ул" - } - }, - "unparsed": [ - "г", - "переулок" - ] - }, - { - "id": "legacy-good-0560", - "raw": "111672, Российская Федерация, 77 Москва г, Москва, Городецкая ул, дом 8, корп. 2", - "mismatches": { - "city": { - "expected": "Москва", - "actual": "77 Москва" - } - }, - "unparsed": [ - "Москва" - ] - }, - { - "id": "legacy-good-0208", - "raw": "142100, Московская обл., г. Подольск, Революционный пр-т, д. 45", - "mismatches": { - "street_type": { - "expected": "пр-кт", - "actual": null - } - }, - "unparsed": [ - "пр-т" - ] - }, - { - "id": "legacy-good-0224", - "raw": "105094, г МОСКВА, ул СЕМЕНОВСКАЯ Б., 42/2-4, 5", - "mismatches": { - "street": { - "expected": "Семёновская Б.", - "actual": "Семеновская Б" - }, - "house_num": { - "expected": "42/2", - "actual": "5" - }, - "corpus": { - "expected": "4", - "actual": null - }, - "apartment": { - "expected": "5", - "actual": null - } - }, - "unparsed": [ - "42/2-4" - ] - }, - { - "id": "legacy-good-0228", - "raw": "105062, г Москва, ул Чаплыгина, 8, 27", - "mismatches": { - "house_num": { - "expected": "8", - "actual": "27" - }, - "apartment": { - "expected": "27", - "actual": null - } - }, - "unparsed": [ - "8" - ] - }, - { - "id": "legacy-good-0128", - "raw": "109004, г. Москва, ул. Николоямская, 49/1-199", - "mismatches": { - "house_num": { - "expected": "49/1", - "actual": "199" - }, - "apartment": { - "expected": "199", - "actual": null - } - }, - "unparsed": [ - "49/1" - ] - }, - { - "id": "legacy-good-0666", - "raw": "119200. г.Москва, Смоленская -Сенная пл., д.32-34", - "mismatches": { - "street": { - "expected": "Смоленская-Сенная", - "actual": "Смоленская -Сенная" - }, - "house_num": { - "expected": "32-34", - "actual": "32" - }, - "apartment": { - "expected": null, - "actual": "34" - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0770", - "raw": "119991, Россия, г.Москва, Ордынка Б. ул., д.25, с.1", - "mismatches": { - "street": { - "expected": "Ордынка Б.", - "actual": "Ордынка Б" - }, - "structure": { - "expected": "1", - "actual": null - } - }, - "unparsed": [ - "с.1" - ] - }, - { - "id": "legacy-good-0340", - "raw": "142100, Российская Федерация, 50 Московская область, Подольск, Комсомольская, 1", - "mismatches": { - "region": { - "expected": "Московская", - "actual": "50 Московская" - }, - "street_type": { - "expected": "ул", - "actual": null - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0970", - "raw": "127055, г. Москва, ул. Новослободская , 14/19 стр.1", - "mismatches": { - "house_num": { - "expected": "14/19", - "actual": "19" - } - }, - "unparsed": [ - "14" - ] - }, - { - "id": "legacy-good-0715", - "raw": "119200, Москва г, ул.Смоленская-Сенная площадь, д.32/34", - "mismatches": { - "street_type": { - "expected": "пл", - "actual": "ул" - } - }, - "unparsed": [ - "г", - "площадь" - ] - }, - { - "id": "legacy-good-0502", - "raw": "123001 г. Москва, ул. Гранатный пер., д. 3, стр.1", - "mismatches": { - "street_type": { - "expected": "пер", - "actual": "ул" - } - }, - "unparsed": [ - "пер" - ] - }, - { - "id": "legacy-good-0086", - "raw": "105062, Москва г, ул.Фурманный переулок, д.10 стр.1", - "mismatches": { - "street_type": { - "expected": "пер", - "actual": "ул" - } - }, - "unparsed": [ - "г", - "переулок" - ] - }, - { - "id": "legacy-good-0160", - "raw": "113405, г.Москва, ул.Варшавское шоссе, д.125Д, корп.1.", - "mismatches": { - "street_type": { - "expected": "ш", - "actual": "ул" - } - }, - "unparsed": [ - "шоссе" - ] - }, - { - "id": "legacy-good-1029", - "raw": "109147, Российская Федерация, г. Москва, г. Москва, Марксистская, 20, строение 6", - "mismatches": { - "street_type": { - "expected": "ул", - "actual": null - } - }, - "unparsed": [ - "г. Москва" - ] - }, - { - "id": "legacy-good-0227", - "raw": "125373, г Москва, проезд Походный, 4, 1, офис 111", - "mismatches": { - "house_num": { - "expected": "4", - "actual": "1" - }, - "corpus": { - "expected": "1", - "actual": null - } - }, - "unparsed": [ - "4" - ] - }, - { - "id": "legacy-good-0753", - "raw": "125212, г.Москва,уп.Адмирала Макарова,д.10,стр.1", - "mismatches": { - "street": { - "expected": "Адмирала Макарова", - "actual": "Макарова" - }, - "street_type": { - "expected": "ул", - "actual": null - } - }, - "unparsed": [ - "уп.Адмирала" - ] - }, - { - "id": "legacy-good-0610", - "raw": "127238, г. Москва. Дмитровское шоссе, д.79", - "mismatches": { - "city": { - "expected": "Москва", - "actual": "Москва. Дмитровское" - }, - "street": { - "expected": "Дмитровское", - "actual": "Шоссе" - }, - "street_type": { - "expected": "ш", - "actual": null - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0908", - "raw": "105094, г. Москва, Гольяновская ул, 7а, стр.1", - "mismatches": { - "house_num": { - "expected": "7А", - "actual": "7" - } - }, - "unparsed": [ - "а" - ] - }, - { - "id": "legacy-good-0241", - "raw": "115573, г. Москва, ул. Ореховый Бульвар, влад. 22Д", - "mismatches": { - "street_type": { - "expected": "б-р", - "actual": "ул" - } - }, - "unparsed": [ - "Бульвар, влад" - ] - }, - { - "id": "legacy-good-0446", - "raw": ", Российская Федерация, Смоленская область, Смоленск, Ново-Ленинградская, 16", - "mismatches": { - "street_type": { - "expected": "ул", - "actual": null - } - }, - "unparsed": [] - }, - { - "id": "legacy-good-0947", - "raw": "125466, г. Москва, Соколово- Мещерская, 16/114", - "mismatches": { - "street": { - "expected": "Соколово-Мещерская", - "actual": "Соколово- Мещерская" - }, - "street_type": { - "expected": "ул", - "actual": null - } - }, - "unparsed": [] - } - ], - "gates": [ - { - "metric": "rows", - "actual": 500, - "minimum": 500, - "passed": true - }, - { - "metric": "exact_address_rate", - "actual": 0.804, - "minimum": 0.8, - "passed": true - }, - { - "metric": "no_unparsed_rate", - "actual": 0.768, - "minimum": 0.75, - "passed": true - }, - { - "metric": "micro.f1", - "actual": 0.958538, - "minimum": 0.955, - "passed": true - }, - { - "metric": "fields.postal_code.f1", - "actual": 1.0, - "minimum": 0.999, - "passed": true - }, - { - "metric": "fields.region.f1", - "actual": 0.905983, - "minimum": 0.9, - "passed": true - }, - { - "metric": "fields.city.f1", - "actual": 0.989919, - "minimum": 0.985, - "passed": true - }, - { - "metric": "fields.street.f1", - "actual": 0.96, - "minimum": 0.955, - "passed": true - }, - { - "metric": "fields.street_type.f1", - "actual": 0.911734, - "minimum": 0.9, - "passed": true - }, - { - "metric": "fields.house_num.f1", - "actual": 0.953815, - "minimum": 0.95, - "passed": true - }, - { - "metric": "fields.corpus.f1", - "actual": 0.958333, - "minimum": 0.95, - "passed": true - }, - { - "metric": "fields.structure.f1", - "actual": 0.970803, - "minimum": 0.96, - "passed": true - }, - { - "metric": "fields.apartment.f1", - "actual": 0.873563, - "minimum": 0.85, - "passed": true - } - ], - "release_gate_passed": true -} diff --git a/evaluation/prepare_datamos.py b/evaluation/prepare_datamos.py deleted file mode 100644 index be1ef1b..0000000 --- a/evaluation/prepare_datamos.py +++ /dev/null @@ -1,274 +0,0 @@ -"""Filter a pinned Moscow official-address snapshot into deterministic splits.""" - -from __future__ import annotations - -import argparse -from collections import Counter -import gzip -import hashlib -import io -import json -from pathlib import Path -import sys -from typing import Any, Iterable -from urllib.request import Request, urlopen -import zipfile - -from datamos_data import ( - ARCHIVE_SHA256, - ARCHIVE_URL, - DATASET_DATE, - DATASET_ID, - DATASET_VERSION, - DEFAULT_ARCHIVE, - DEFAULT_FILTERED, - DEFAULT_MANIFEST, - INNER_DATA_SHA256, - SOURCE_ROWS, - expected_components, - fold, - group_id_and_split, - quality_tier, - rejection_reason, -) - - -def _sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as source: - for chunk in iter(lambda: source.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def _archive_member_sha256(path: Path, member: str) -> str: - digest = hashlib.sha256() - with zipfile.ZipFile(path) as archive: - with archive.open(member) as source: - for chunk in iter(lambda: source.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def download_source(path: Path) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - temporary = path.with_suffix(f"{path.suffix}.part") - request = Request(ARCHIVE_URL, headers={"User-Agent": "address-normalizer/2"}) - with urlopen(request, timeout=120) as response, temporary.open("wb") as output: - while chunk := response.read(1024 * 1024): - output.write(chunk) - actual = _sha256(temporary) - if actual != ARCHIVE_SHA256: - temporary.unlink(missing_ok=True) - raise ValueError( - f"archive checksum mismatch: expected {ARCHIVE_SHA256}, got {actual}" - ) - temporary.replace(path) - - -def _documents(path: Path) -> Iterable[dict[str, Any]]: - try: - from bson import decode_file_iter - except ModuleNotFoundError as error: - raise SystemExit( - "PyMongo is required only for Moscow dataset preparation. Install " - "`requirements-evaluation.txt` in a separate environment." - ) from error - with zipfile.ZipFile(path) as archive: - with archive.open("data.bson.gz") as compressed_source: - with gzip.GzipFile(fileobj=compressed_source, mode="rb") as source: - yield from decode_file_iter(source) - - -def prepare( - source: Path, - filtered: Path, - manifest_path: Path, - *, - overwrite: bool, -) -> dict[str, Any]: - for path in (filtered, manifest_path): - if path.exists() and not overwrite: - raise FileExistsError(f"{path} already exists; pass --overwrite") - actual_archive_sha256 = _sha256(source) - if actual_archive_sha256 != ARCHIVE_SHA256: - raise ValueError( - f"archive checksum mismatch: expected {ARCHIVE_SHA256}, " - f"got {actual_archive_sha256}" - ) - actual_inner_sha256 = _archive_member_sha256(source, "data.bson.gz") - if actual_inner_sha256 != INNER_DATA_SHA256: - raise ValueError( - f"inner data checksum mismatch: expected {INNER_DATA_SHA256}, " - f"got {actual_inner_sha256}" - ) - - filtered.parent.mkdir(parents=True, exist_ok=True) - manifest_path.parent.mkdir(parents=True, exist_ok=True) - temporary = filtered.with_suffix(f"{filtered.suffix}.part") - counts: Counter[str] = Counter() - seen: set[str] = set() - with temporary.open("wb") as raw_output: - with gzip.GzipFile( - filename="", - mode="wb", - fileobj=raw_output, - mtime=0, - ) as compressed: - with io.TextIOWrapper(compressed, encoding="utf-8") as output: - for source_row, row in enumerate(_documents(source)): - counts["source_rows"] += 1 - reason = rejection_reason(row) - if reason is not None: - counts[f"rejected_{reason}"] += 1 - continue - raw = str(row["SIMPLE_ADDRESS"]).strip() - normalized = fold(raw) - if normalized in seen: - counts["duplicate_rows_removed"] += 1 - continue - seen.add(normalized) - group_id, split = group_id_and_split(row) - tier = quality_tier(row) - record = { - "source_row": source_row, - "group_id": group_id, - "split": split, - "tier": tier, - "raw": raw, - "legal_address": str(row["ADDRESS"]).strip(), - "expected": expected_components(row), - "fias_id": str(row["N_FIAS"]).lower(), - "unom": row.get("UNOM"), - "registry_id": row.get("NREG"), - "object_type": row.get("OBJ_TYPE"), - } - output.write( - json.dumps( - record, - ensure_ascii=False, - separators=(",", ":"), - ) - ) - output.write("\n") - counts["unique_usable_rows"] += 1 - counts[f"split_{split}"] += 1 - counts[f"tier_{tier}"] += 1 - counts[f"object_{row.get('OBJ_TYPE')}"] += 1 - if counts["source_rows"] != SOURCE_ROWS: - temporary.unlink(missing_ok=True) - raise ValueError( - f"unexpected source row count: expected {SOURCE_ROWS}, " - f"got {counts['source_rows']}" - ) - temporary.replace(filtered) - - manifest = { - "format_version": 1, - "source": { - "title": ( - "Адресный реестр объектов недвижимости города Москвы" - ), - "publisher": ( - "Департамент городского имущества города Москвы" - ), - "original_portal": "https://data.mos.ru", - "dataset_id": DATASET_ID, - "version": DATASET_VERSION, - "release_date": DATASET_DATE, - "mirror": ( - "https://data2.apicrafter.ru/packages/" - "datamos-addressreestr" - ), - "archive_url": ARCHIVE_URL, - "archive_bytes": source.stat().st_size, - "archive_sha256": actual_archive_sha256, - "inner_data_sha256": actual_inner_sha256, - "source_rows": SOURCE_ROWS, - "embedded_terms": ( - "Типовые условия доступа к открытым данным органов власти в РФ" - ), - "mirror_terms": "CC-BY-SA", - }, - "policy": { - "purpose": ( - "Moscow-only official clean-address training/evaluation corpus; " - "not bundled in the runtime package" - ), - "filter": [ - "OnTerritoryOfMoscow == да", - "ADR_TYPE == Официальный", - "SOSTAD == Зарегистрирован в АР", - "STATUS == Внесён в ГКН", - "non-empty SIMPLE_ADDRESS, P7 street, and L1_VALUE house", - "valid N_FIAS UUID", - ], - "deduplication": ( - "first case-folded, ё/е-folded, whitespace-normalized " - "SIMPLE_ADDRESS" - ), - "grouping": ( - "street/house/corpus/structure identity; SHA-256 groups are " - "assigned 90% train, 5% validation, 5% test" - ), - }, - "counts": dict(sorted(counts.items())), - "artifact": { - "filename": filtered.name, - "rows": counts["unique_usable_rows"], - "bytes": filtered.stat().st_size, - "sha256": _sha256(filtered), - }, - "limitations": [ - "The snapshot is from October 2021 and is not a current registry.", - "The corpus is Moscow-only and consists of clean legal formatting.", - ( - "Only street, house, corpus, and structure are scored from " - "SIMPLE_ADDRESS; administrative fields are intentionally out " - "of scope for this view." - ), - ( - "The archive is obtained from an attributed mirror; retain its " - "embedded metadata and confirm redistribution terms before " - "publishing derived rows." - ), - ], - } - rendered = f"{json.dumps(manifest, ensure_ascii=False, indent=2)}\n" - temporary_manifest = manifest_path.with_suffix( - f"{manifest_path.suffix}.part" - ) - temporary_manifest.write_text(rendered, encoding="utf-8") - temporary_manifest.replace(manifest_path) - return manifest - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--source", type=Path, default=DEFAULT_ARCHIVE) - parser.add_argument("--filtered", type=Path, default=DEFAULT_FILTERED) - parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST) - parser.add_argument("--download", action="store_true") - parser.add_argument("--overwrite", action="store_true") - args = parser.parse_args(argv) - if args.download: - download_source(args.source) - if not args.source.exists(): - parser.error( - f"{args.source} does not exist; provide --source or use --download" - ) - try: - manifest = prepare( - args.source, - args.filtered, - args.manifest, - overwrite=args.overwrite, - ) - except (FileExistsError, ValueError) as error: - parser.error(str(error)) - print(json.dumps(manifest, ensure_ascii=False, indent=2)) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/evaluation/prepare_deepparse.py b/evaluation/prepare_deepparse.py deleted file mode 100644 index 58d6e1c..0000000 --- a/evaluation/prepare_deepparse.py +++ /dev/null @@ -1,487 +0,0 @@ -"""Prepare the full Deepparse Russian shard for training and evaluation.""" - -from __future__ import annotations - -import argparse -from collections import Counter -import gzip -import hashlib -import heapq -import io -import json -from pathlib import Path -import sqlite3 -import sys -from typing import Any, Iterable -from urllib.request import Request, urlopen - -from deepparse_data import ( - DATASET_REVISION, - DATASET_ROWS, - DATASET_SHA256, - DATASET_URL, - DEFAULT_FILTERED, - DEFAULT_MANIFEST, - DEFAULT_SAMPLE, - DEFAULT_SOURCE, - EXPECTED_FIELDS, - SOURCE_TAGS, - USEFUL_TAGS, - expected_components, - group_id_and_split, - mapped_labels, - normalized_address_id, - quality_tier, - sample_rank, -) - - -BATCH_SIZE = 131_072 -WRITE_BATCH_SIZE = 65_536 - - -def _sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as source: - for chunk in iter(lambda: source.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def download_source(path: Path) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - temporary = path.with_suffix(f"{path.suffix}.part") - request = Request(DATASET_URL, headers={"User-Agent": "address-normalizer/2"}) - with urlopen(request, timeout=120) as response, temporary.open("wb") as output: - while chunk := response.read(1024 * 1024): - output.write(chunk) - actual = _sha256(temporary) - if actual != DATASET_SHA256: - temporary.unlink(missing_ok=True) - raise ValueError( - f"source checksum mismatch: expected {DATASET_SHA256}, got {actual}" - ) - temporary.replace(path) - - -def _require_pyarrow() -> tuple[Any, Any]: - try: - import pyarrow as pa - import pyarrow.parquet as pq - except ModuleNotFoundError as error: - raise SystemExit( - "PyArrow is required only for dataset preparation. Install " - "`requirements-evaluation.txt` in a separate environment." - ) from error - return pa, pq - - -def _schema(pa: Any) -> Any: - expected = pa.struct([(field, pa.string()) for field in EXPECTED_FIELDS]) - return pa.schema( - [ - ("source_row", pa.int64()), - ("example_id", pa.binary(16)), - ("group_id", pa.binary(16)), - ("split", pa.string()), - ("tier", pa.string()), - ("language", pa.string()), - ("raw", pa.string()), - ("tokens", pa.list_(pa.string())), - ("source_tags", pa.list_(pa.string())), - ("labels", pa.list_(pa.string())), - ("expected", expected), - ] - ) - - -def _source_batches(parquet_file: Any) -> Iterable[tuple[list[Any], ...]]: - for batch in parquet_file.iter_batches(batch_size=BATCH_SIZE): - yield tuple(column.to_pylist() for column in batch.columns) - - -def _validate_source(parquet_file: Any) -> None: - required = {"Address", "Tags", "Language"} - actual = set(parquet_file.schema_arrow.names) - if actual != required: - raise ValueError( - f"unexpected source columns: expected {sorted(required)}, " - f"got {sorted(actual)}" - ) - if parquet_file.metadata.num_rows != DATASET_ROWS: - raise ValueError( - f"unexpected row count: expected {DATASET_ROWS}, " - f"got {parquet_file.metadata.num_rows}" - ) - - -def _open_dedupe_database(path: Path) -> sqlite3.Connection: - connection = sqlite3.connect(path) - connection.execute("PRAGMA journal_mode=OFF") - connection.execute("PRAGMA synchronous=OFF") - connection.execute("PRAGMA temp_store=MEMORY") - connection.execute("PRAGMA locking_mode=EXCLUSIVE") - connection.execute( - "CREATE TABLE seen (example_id BLOB PRIMARY KEY, source_row INTEGER) " - "WITHOUT ROWID" - ) - return connection - - -def _mark_accepted_rows( - parquet_file: Any, - database: sqlite3.Connection, -) -> tuple[bytearray, dict[str, int]]: - stats: Counter[str] = Counter() - source_row = 0 - for addresses, tag_lists, languages in _source_batches(parquet_file): - candidates: list[tuple[bytes, int]] = [] - for address, tags, language in zip(addresses, tag_lists, languages): - stats["source_rows"] += 1 - tokens = address.split() if isinstance(address, str) else [] - if language != "rus": - stats["rejected_language"] += 1 - elif not tokens: - stats["rejected_empty"] += 1 - elif len(tokens) != len(tags): - stats["rejected_token_tag_mismatch"] += 1 - elif len(tokens) > 64: - stats["rejected_too_long"] += 1 - elif not set(tags) <= SOURCE_TAGS: - stats["rejected_unknown_tag"] += 1 - elif not set(tags) & USEFUL_TAGS: - stats["rejected_administrative_only"] += 1 - else: - stats["eligible_rows"] += 1 - candidates.append((normalized_address_id(address), source_row)) - source_row += 1 - database.executemany( - "INSERT OR IGNORE INTO seen(example_id, source_row) VALUES (?, ?)", - candidates, - ) - database.commit() - - unique_rows = int(database.execute("SELECT count(*) FROM seen").fetchone()[0]) - stats["unique_usable_rows"] = unique_rows - stats["duplicate_rows_removed"] = stats["eligible_rows"] - unique_rows - accepted = bytearray((stats["source_rows"] + 7) // 8) - for (row_number,) in database.execute("SELECT source_row FROM seen"): - accepted[row_number >> 3] |= 1 << (row_number & 7) - return accepted, dict(stats) - - -def _is_accepted(accepted: bytearray, source_row: int) -> bool: - return bool(accepted[source_row >> 3] & (1 << (source_row & 7))) - - -def _write_records( - writer: Any, - pa: Any, - schema: Any, - records: list[dict[str, Any]], -) -> None: - if records: - writer.write_table(pa.Table.from_pylist(records, schema=schema)) - records.clear() - - -def _write_filtered( - parquet_file: Any, - accepted: bytearray, - output: Path, - sample_size: int, - pa: Any, - pq: Any, -) -> tuple[Counter[tuple[str, str]], set[int]]: - schema = _schema(pa) - temporary = output.with_suffix(f"{output.suffix}.part") - writer = pq.ParquetWriter( - temporary, - schema, - compression="zstd", - compression_level=6, - use_dictionary=["split", "tier", "language", "source_tags", "labels"], - write_statistics=True, - ) - records: list[dict[str, Any]] = [] - sample_heap: list[tuple[int, int]] = [] - counts: Counter[tuple[str, str]] = Counter() - source_row = 0 - try: - for addresses, tag_lists, languages in _source_batches(parquet_file): - for address, tags, language in zip( - addresses, tag_lists, languages - ): - if not _is_accepted(accepted, source_row): - source_row += 1 - continue - tokens = address.split() - labels = mapped_labels(tags) - example_id = normalized_address_id(address) - group_id, split = group_id_and_split(tokens, tags) - tier = quality_tier(tags) - records.append( - { - "source_row": source_row, - "example_id": example_id, - "group_id": group_id, - "split": split, - "tier": tier, - "language": language, - "raw": address, - "tokens": tokens, - "source_tags": tags, - "labels": labels, - "expected": expected_components(tokens, labels), - } - ) - counts[(split, tier)] += 1 - if split == "test" and sample_size: - rank = sample_rank(example_id) - candidate = (-rank, source_row) - if len(sample_heap) < sample_size: - heapq.heappush(sample_heap, candidate) - elif candidate > sample_heap[0]: - heapq.heapreplace(sample_heap, candidate) - if len(records) >= WRITE_BATCH_SIZE: - _write_records(writer, pa, schema, records) - source_row += 1 - _write_records(writer, pa, schema, records) - except BaseException: - writer.close() - temporary.unlink(missing_ok=True) - raise - writer.close() - temporary.replace(output) - return counts, {source_row for _, source_row in sample_heap} - - -def _write_sample( - filtered: Path, - output: Path, - selected_rows: set[int], - pq: Any, -) -> int: - temporary = output.with_suffix(f"{output.suffix}.part") - written = 0 - with temporary.open("wb") as raw_output: - with gzip.GzipFile( - filename="", - mode="wb", - fileobj=raw_output, - mtime=0, - ) as compressed: - with io.TextIOWrapper(compressed, encoding="utf-8") as text: - parquet_file = pq.ParquetFile(filtered) - for batch in parquet_file.iter_batches(batch_size=BATCH_SIZE): - for row in batch.to_pylist(): - if row["source_row"] not in selected_rows: - continue - row["example_id"] = row["example_id"].hex() - row["group_id"] = row["group_id"].hex() - text.write( - json.dumps( - row, - ensure_ascii=False, - separators=(",", ":"), - ) - ) - text.write("\n") - written += 1 - temporary.replace(output) - return written - - -def prepare( - source: Path, - filtered: Path, - sample: Path, - manifest_path: Path, - *, - sample_size: int, - overwrite: bool, -) -> dict[str, Any]: - if sample_size < 0: - raise ValueError("sample_size must be non-negative") - for path in (filtered, sample, manifest_path): - if path.exists() and not overwrite: - raise FileExistsError(f"{path} already exists; pass --overwrite") - actual_sha256 = _sha256(source) - if actual_sha256 != DATASET_SHA256: - raise ValueError( - f"source checksum mismatch: expected {DATASET_SHA256}, " - f"got {actual_sha256}" - ) - - pa, pq = _require_pyarrow() - parquet_file = pq.ParquetFile(source) - _validate_source(parquet_file) - filtered.parent.mkdir(parents=True, exist_ok=True) - sample.parent.mkdir(parents=True, exist_ok=True) - manifest_path.parent.mkdir(parents=True, exist_ok=True) - database_path = filtered.with_suffix(".dedupe.sqlite3") - database_path.unlink(missing_ok=True) - database = _open_dedupe_database(database_path) - try: - accepted, filter_counts = _mark_accepted_rows(parquet_file, database) - finally: - database.close() - - parquet_file = pq.ParquetFile(source) - counts, selected_rows = _write_filtered( - parquet_file, - accepted, - filtered, - sample_size, - pa, - pq, - ) - written_sample_rows = _write_sample( - filtered, - sample, - selected_rows, - pq, - ) - if written_sample_rows != min( - sample_size, - sum(count for (split, _), count in counts.items() if split == "test"), - ): - raise RuntimeError("benchmark sample row count does not match selection") - database_path.unlink(missing_ok=True) - - split_counts = { - split: sum( - count for (candidate, _), count in counts.items() - if candidate == split - ) - for split in ("train", "validation", "test") - } - tier_counts = { - tier: sum( - count for (_, candidate), count in counts.items() - if candidate == tier - ) - for tier in ( - "street_house_unit", - "street_house", - "street_only", - "number_or_unit_only", - ) - } - manifest = { - "format_version": 1, - "source": { - "repository": "deepparse/worldwide-addresses", - "configuration": "ru", - "license": "CC BY 4.0", - "revision": DATASET_REVISION, - "url": DATASET_URL, - "rows": DATASET_ROWS, - "bytes": source.stat().st_size, - "sha256": actual_sha256, - }, - "policy": { - "purpose": ( - "external clean-address training/evaluation corpus; not bundled " - "in the runtime package" - ), - "filter": ( - "Russian rows with 1-64 whitespace tokens, known tags, matching " - "token/tag lengths, and at least one StreetName, StreetNumber, " - "or Unit tag" - ), - "deduplication": ( - "first row for each BLAKE2b-128 hash of case-folded, ё/е-folded, " - "whitespace-normalized address text" - ), - "grouping": ( - "SHA-256 of structured province/county/district/municipality/" - "suburb/street/house identity; unit and presentation fields are " - "excluded so one building cannot cross splits" - ), - "split": "group hash buckets: train 90%, validation 5%, test 5%", - "sample": ( - "lowest deterministic SHA-256 ranks from the sealed test split" - ), - "ignored_labels": ["Country", "Suburb"], - }, - "filter_counts": filter_counts, - "split_counts": split_counts, - "tier_counts": tier_counts, - "split_tier_counts": { - f"{split}/{tier}": count - for (split, tier), count in sorted(counts.items()) - }, - "artifacts": { - "filtered_parquet": { - "filename": filtered.name, - "rows": sum(split_counts.values()), - "bytes": filtered.stat().st_size, - "sha256": _sha256(filtered), - }, - "test_sample_jsonl_gz": { - "filename": sample.name, - "rows": written_sample_rows, - "bytes": sample.stat().st_size, - "sha256": _sha256(sample), - }, - }, - "limitations": [ - ( - "The source is curated from open geographic address data and " - "does not reproduce misspellings or punctuation-heavy user input." - ), - ( - "Country and Suburb have no direct public package field and are " - "kept as source context but mapped to O." - ), - ( - "This deterministic test split becomes tuning data after its " - "failures are used to change the parser; reserve another test " - "source before making a final production claim." - ), - ], - } - rendered = f"{json.dumps(manifest, ensure_ascii=False, indent=2)}\n" - temporary_manifest = manifest_path.with_suffix( - f"{manifest_path.suffix}.part" - ) - temporary_manifest.write_text(rendered, encoding="utf-8") - temporary_manifest.replace(manifest_path) - return manifest - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE) - parser.add_argument("--filtered", type=Path, default=DEFAULT_FILTERED) - parser.add_argument("--sample", type=Path, default=DEFAULT_SAMPLE) - parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST) - parser.add_argument("--sample-size", type=int, default=100_000) - parser.add_argument("--download", action="store_true") - parser.add_argument("--overwrite", action="store_true") - args = parser.parse_args(argv) - - if args.download: - download_source(args.source) - if not args.source.exists(): - parser.error( - f"{args.source} does not exist; provide --source or use --download" - ) - try: - report = prepare( - args.source, - args.filtered, - args.sample, - args.manifest, - sample_size=args.sample_size, - overwrite=args.overwrite, - ) - except (FileExistsError, ValueError) as error: - parser.error(str(error)) - print(json.dumps(report, ensure_ascii=False, indent=2)) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/evaluation/redmadrobot_detection_report.json b/evaluation/redmadrobot_detection_report.json deleted file mode 100644 index 0916650..0000000 --- a/evaluation/redmadrobot_detection_report.json +++ /dev/null @@ -1,2758 +0,0 @@ -{ - "rows": 2841, - "positive_rows": 135, - "negative_rows": 2706, - "scope": "complete reconstructed benchmark messages; gold address windows must contain both STREET and HOUSE labels", - "limitations": [ - "The source is a PII NER benchmark, not a detector-specific Russian message sample.", - "Gold spans follow BIO annotation boundaries while predicted spans intentionally include parseable markers and units.", - "Rows without a STREET+HOUSE cluster are treated as detection negatives even when they contain isolated location entities." - ], - "metric_definitions": { - "span_overlap_micro": "one-to-one precision, recall, and F1 for any character overlap between a detected span and a STREET+HOUSE gold window", - "exact_span_micro": "one-to-one precision, recall, and F1 requiring identical half-open boundaries", - "exact_message_rate": "fraction of complete messages whose ordered span lists match", - "negative_message_specificity": "fraction of messages without a STREET+HOUSE gold window where the detector returns no span" - }, - "span_overlap_micro": { - "tp": 98, - "fp": 2, - "fn": 46, - "precision": 0.98, - "recall": 0.680556, - "f1": 0.803279 - }, - "exact_span_micro": { - "tp": 29, - "fp": 71, - "fn": 115, - "precision": 0.29, - "recall": 0.201389, - "f1": 0.237705 - }, - "exact_message_rate": 0.962337, - "negative_message_specificity": 1.0, - "failure_case_count": 107, - "failure_rows_by_reason": { - "missed_address": 44, - "span_drops_gold_text": 33, - "span_includes_context": 33, - "spurious_address": 1 - }, - "failure_cases": [ - { - "source_row": 0, - "text": "ООО « Ремонт и Обслуживание » УЛ . РОКОССОВСКОГО , дом 10 , офис 15 , ВОЛЬСК , 191023 , Танзания ТЕЛ . : +7 ( 812 ) 987 6543 , WWW . REMONT-I-OBSLUZHIVANIE . RU", - "gold": [ - { - "start": 30, - "end": 96, - "text": "УЛ . РОКОССОВСКОГО , дом 10 , офис 15 , ВОЛЬСК , 191023 , Танзания" - } - ], - "predicted": [ - { - "start": 30, - "end": 67, - "text": "УЛ . РОКОССОВСКОГО , дом 10 , офис 15", - "confidence": 0.77, - "signals": [ - "street_marker", - "house_marker", - "unit_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 54, - "text": "ул . Крымский Вал , 10 , Бузулук , 119049 Форма заявки: на https://help . bitbucket . org/terms .", - "gold": [ - { - "start": 0, - "end": 32, - "text": "ул . Крымский Вал , 10 , Бузулук" - } - ], - "predicted": [ - { - "start": 0, - "end": 22, - "text": "ул . Крымский Вал , 10", - "confidence": 0.44, - "signals": [ - "street_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 74, - "text": "Адрес: Leonard Назаровна терехов , пер . Маршала Жукова , д . 181 .", - "gold": [ - { - "start": 35, - "end": 65, - "text": "пер . Маршала Жукова , д . 181" - } - ], - "predicted": [ - { - "start": 7, - "end": 65, - "text": "Leonard Назаровна терехов , пер . Маршала Жукова , д . 181", - "confidence": 0.85, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 118, - "text": "ООО \" Спортивный мир \" 630005 , Регион 63 , Невинномысск , улица Кирова , дом 25 , квартира 10 ИНН 6312345678 Место расчётов: sportivnyimir . ru", - "gold": [ - { - "start": 44, - "end": 94, - "text": "Невинномысск , улица Кирова , дом 25 , квартира 10" - } - ], - "predicted": [ - { - "start": 59, - "end": 94, - "text": "улица Кирова , дом 25 , квартира 10", - "confidence": 0.77, - "signals": [ - "street_marker", - "house_marker", - "unit_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 151, - "text": "Клиника: адрес пр . бауманская , д . 71 . Запись по email: timur . morozov@protonmail . com .", - "gold": [ - { - "start": 15, - "end": 39, - "text": "пр . бауманская , д . 71" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 218, - "text": "Адрес: 2 этаж , центр ТОРГОВЫЙ КВАРТАЛ 3-а , Каширское ш . , Вольск , Глазов , АМУРСКАЯ ОБЛ . , 142000", - "gold": [ - { - "start": 39, - "end": 91, - "text": "3-а , Каширское ш . , Вольск , Глазов , АМУРСКАЯ ОБЛ" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 223, - "text": "Заявитель: Пестов Reginald Демьяновна , адрес: г . Чапаевск , ш . Ярцевская , д . 32 , email: beth09@example . org .", - "gold": [ - { - "start": 51, - "end": 84, - "text": "Чапаевск , ш . Ярцевская , д . 32" - } - ], - "predicted": [ - { - "start": 47, - "end": 84, - "text": "г . Чапаевск , ш . Ярцевская , д . 32", - "confidence": 0.91, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 232, - "text": "В соответствии с запросом котировки № 54321 , ООО « Луч » предоставляет следующие сведения : ИНН 7707083893 , адрес : г . Златоуст , ул . Примерная , д . 1 . Просим учесть наши предложения при рассмотрении заявки .", - "gold": [ - { - "start": 122, - "end": 155, - "text": "Златоуст , ул . Примерная , д . 1" - } - ], - "predicted": [ - { - "start": 118, - "end": 155, - "text": "г . Златоуст , ул . Примерная , д . 1", - "confidence": 0.91, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 246, - "text": "MOY ADRES PROPISKI: АБХАЗИЯ , almatinskaya OBLAST G . TALDYKORGAN ul . abaya D . 10 KV . 4", - "gold": [ - { - "start": 20, - "end": 90, - "text": "АБХАЗИЯ , almatinskaya OBLAST G . TALDYKORGAN ul . abaya D . 10 KV . 4" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 262, - "text": "MOY ADRES PROPISKI: ROSSIYA , тАмБовсКАя OBLAST G . MYTISHCHI Стромынка D . 5 KV . 12", - "gold": [ - { - "start": 20, - "end": 85, - "text": "ROSSIYA , тАмБовсКАя OBLAST G . MYTISHCHI Стромынка D . 5 KV . 12" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 274, - "text": "УВЕДОМЛЕНИЕ об одностороннем отказе от исполнения договора возмездного оказания услуг от 04 . 03 . 2026 и окончательном расчёте Кому : Жолобовой Маргарите Петровне e - mail : golobovaantonina @ gmail . com От : Общества с ограниченной ответственностью « Юридическая группа ТАРАН » адрес : 655917 , Республика Хакасия , г . Абакан , пр . Ленина , д . 29А , пом . 8Н ИНН 1902029948 , ОГРН 1201900001885 Дата : 28 апреля 2026 г . Уважаемая Алина Ивановна ! Между Вами ( Исполнитель ) и ООО « Юридическая группа ТАРЗАН » ( Заказчик ) заключён договор возмездного оказания услуг от 04 марта 2026 г . ( далее — Договор ) . С 22 апреля 2021 г . Вы в одностор . . . . . . соответствует ранее направленному Акту от 21 апреля 2026 г . , составленному в связи с фактическим прекращением оказания услуг . Сумма налога на доходы физических лиц , подлежащая удержанию , — 4 334 руб . Таким образом , к перечислению на Ваш расчётный счёт причитается 28 999 ( Двадцать восемь тысяч девятьсот девяносто девять ) рублей 00 копеек . Указанная сумма будет перечислена в течение 10 ( десяти ) банковских дней с даты получения Вами настоящего уведомления . Иные выплаты , включая полное фиксированное вознаграждение за апрель , компенсации и неустойки , Заказчиком не производятся , поскольку услуги в полном объёме и надлежащим образом не оказывались , а Договор правомерно прекращён . Направление дополнительных актов или претензий со стороны Исполнителя не повлияет на изложенную позицию : Договор расторгнут Заказчиком в одностороннем порядке в соответствии с императивной нормой ст . 782 ГК РФ , и надлежащее прекращение обязательств не требует подписания двустороннего соглашения . В случае возникновения судебного спора Заказчик оставляет за собой право предъявить требования о взыскании убытков , вызванных некачественным оказанием услуг ( в том числе упущенной выгоды вследствие потери клиентов из - за пропущенных обращений ) , а также о возмещении всех судебных издержек . Проверить на безопасность по законодательству для заказчика", - "gold": [ - { - "start": 298, - "end": 353, - "text": "Республика Хакасия , г . Абакан , пр . Ленина , д . 29А" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 293, - "text": "kservice Кузовной сервис №1 в Невинномысск г . УСОЛЬЕ-СИБИРСКОЕ , УЛ . ДУБИНИНСКАЯ , 70 стр . 1 ( ~ 14000 без стоимости бампера и без дополнительных работ )", - "gold": [ - { - "start": 30, - "end": 95, - "text": "Невинномысск г . УСОЛЬЕ-СИБИРСКОЕ , УЛ . ДУБИНИНСКАЯ , 70 стр . 1" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 350, - "text": "+79772711425 , адрес: пр . Хавская , д . 149 , кв . 230 .", - "gold": [ - { - "start": 22, - "end": 55, - "text": "пр . Хавская , д . 149 , кв . 230" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 407, - "text": "Получатель: Greenwood Percy Зиновьевна , адрес: пр . Школьная , дом 101 , кв . 266 .", - "gold": [ - { - "start": 48, - "end": 82, - "text": "пр . Школьная , дом 101 , кв . 266" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 424, - "text": "Japan cars сервис - ТО и ремонт автомобилей 924 797 21 83 Авдотья - Старокоптевский пер . , 6 ( м . Войковская )", - "gold": [ - { - "start": 68, - "end": 93, - "text": "Старокоптевский пер . , 6" - } - ], - "predicted": [ - { - "start": 0, - "end": 93, - "text": "Japan cars сервис - ТО и ремонт автомобилей 924 797 21 83 Авдотья - Старокоптевский пер . , 6", - "confidence": 0.44, - "signals": [ - "street_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 467, - "text": "Ping to 26 . 204 . 205 . 243 successful . Facebook , Inc . 1 Hacker Way , MENLO PARK , УЛЬЯНОВСКАЯ 94025 БАНГЛАДЕШ", - "gold": [ - { - "start": 59, - "end": 114, - "text": "1 Hacker Way , MENLO PARK , УЛЬЯНОВСКАЯ 94025 БАНГЛАДЕШ" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 500, - "text": "братан 181 . 61 . 145 . 201:5432 это наш прод . Штрих-код 4607038490125 , доставить в Уфу , пер . Заводской , стр . 2", - "gold": [ - { - "start": 86, - "end": 117, - "text": "Уфу , пер . Заводской , стр . 2" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 533, - "text": "Проверили данные по контрагенту ООО \" Ромашка \" . Юридический адрес : 123456 , г . Невинномысск , ул . Ленина , д . 1 . ИНН : 7707083893 , КПП : 770101001 . Необходимо запросить у них свежую выписку из ЕГРЮЛ .", - "gold": [ - { - "start": 83, - "end": 117, - "text": "Невинномысск , ул . Ленина , д . 1" - } - ], - "predicted": [ - { - "start": 70, - "end": 117, - "text": "123456 , г . Невинномысск , ул . Ленина , д . 1", - "confidence": 0.99, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "postal_code", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 556, - "text": "г Алатырь , наб Новоданиловская , 10 , 3 . 318", - "gold": [ - { - "start": 2, - "end": 46, - "text": "Алатырь , наб Новоданиловская , 10 , 3 . 318" - } - ], - "predicted": [ - { - "start": 0, - "end": 36, - "text": "г Алатырь , наб Новоданиловская , 10", - "confidence": 0.5, - "signals": [ - "street_marker", - "location_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text", - "span_includes_context" - ] - }, - { - "source_row": 607, - "text": "ООО \" Яндекс . Маркет \" 119021 , Бузулук , УЛИЦА ЛЬВА ТОЛСТОГО , дом 16 , строение 2 ИНН 7705396987 Место расчётов: market . yandex . ru", - "gold": [ - { - "start": 33, - "end": 84, - "text": "Бузулук , УЛИЦА ЛЬВА ТОЛСТОГО , дом 16 , строение 2" - } - ], - "predicted": [ - { - "start": 43, - "end": 84, - "text": "УЛИЦА ЛЬВА ТОЛСТОГО , дом 16 , строение 2", - "confidence": 0.77, - "signals": [ - "street_marker", - "house_marker", - "unit_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 693, - "text": "Претензия направляется в адрес ООО \" Строительная Компания \" ИНН 7707083893 . Мы требуем немедленно устранить недоделки , выявленные при приемке объекта по адресу : г . Чапаевск , ул . Строителей , д . 1 . В противном случае мы будем вынуждены обратиться в суд .", - "gold": [ - { - "start": 169, - "end": 203, - "text": "Чапаевск , ул . Строителей , д . 1" - } - ], - "predicted": [ - { - "start": 165, - "end": 203, - "text": "г . Чапаевск , ул . Строителей , д . 1", - "confidence": 0.91, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 724, - "text": "Автосервис « Лихач » Невинномысск , 2-Й ЛИХАЧЁВСКИЙ ПЕРЕУЛОК , 10с1 ( - )", - "gold": [ - { - "start": 21, - "end": 67, - "text": "Невинномысск , 2-Й ЛИХАЧЁВСКИЙ ПЕРЕУЛОК , 10с1" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 764, - "text": "MOY ADRES PROPISKI: ROSSIYA , LENINGRADSKAYA OBLAST G . SAINT-PETERSBURG UL . NEVSKYI PROSPEKT D . 20 KV . 7", - "gold": [ - { - "start": 20, - "end": 108, - "text": "ROSSIYA , LENINGRADSKAYA OBLAST G . SAINT-PETERSBURG UL . NEVSKYI PROSPEKT D . 20 KV . 7" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 774, - "text": "NOVOSIBIRSK , gagARiNA 7", - "gold": [ - { - "start": 0, - "end": 24, - "text": "NOVOSIBIRSK , gagARiNA 7" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 843, - "text": "Адрес: офис на просп . Комсомольская , д . 15 . Сайт: https://app . mchs . gov . ru .", - "gold": [ - { - "start": 15, - "end": 45, - "text": "просп . Комсомольская , д . 15" - } - ], - "predicted": [ - { - "start": 7, - "end": 45, - "text": "офис на просп . Комсомольская , д . 15", - "confidence": 0.85, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 871, - "text": "MOY ADRES PROPISKI: МАЛЬТА , ALMATINSKAYA OBLAST G . almaty UL . DOSTYK D . 15 KV . 20", - "gold": [ - { - "start": 20, - "end": 86, - "text": "МАЛЬТА , ALMATINSKAYA OBLAST G . almaty UL . DOSTYK D . 15 KV . 20" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 872, - "text": "ООО \" Рога и копыта \" 115487 , Регион 77 , Чапаевск , Дмитровское шоссе , дом 10 , строение 2 ИНН 7701234567 Место расчётов: rogaikopyta . ru", - "gold": [ - { - "start": 43, - "end": 93, - "text": "Чапаевск , Дмитровское шоссе , дом 10 , строение 2" - } - ], - "predicted": [ - { - "start": 54, - "end": 93, - "text": "Дмитровское шоссе , дом 10 , строение 2", - "confidence": 0.77, - "signals": [ - "street_marker", - "house_marker", - "unit_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 887, - "text": "По адресу УЛ . МАЯКОВСКОГО , д . 7 , г . Минск , Ботсвана", - "gold": [ - { - "start": 10, - "end": 57, - "text": "УЛ . МАЯКОВСКОГО , д . 7 , г . Минск , Ботсвана" - } - ], - "predicted": [ - { - "start": 10, - "end": 34, - "text": "УЛ . МАЯКОВСКОГО , д . 7", - "confidence": 0.85, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 951, - "text": "Офис в Южном федеральном округе , Краснодар , бульвар Платановый 7", - "gold": [ - { - "start": 7, - "end": 66, - "text": "Южном федеральном округе , Краснодар , бульвар Платановый 7" - } - ], - "predicted": [ - { - "start": 46, - "end": 66, - "text": "бульвар Платановый 7", - "confidence": 0.44, - "signals": [ - "street_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 953, - "text": "Уведомление о прибытии : ФИО : Устинов А . В . Регион : MYTISHCHI . Адрес : ул . Ленина , д . 1 . Паспортные данные : 4500 123456 . ИНН : 500100732259 .", - "gold": [ - { - "start": 56, - "end": 95, - "text": "MYTISHCHI . Адрес : ул . Ленина , д . 1" - } - ], - "predicted": [ - { - "start": 76, - "end": 95, - "text": "ул . Ленина , д . 1", - "confidence": 0.85, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 963, - "text": "Склад: Свердловская область , Берёзовский , Транспортная 8А", - "gold": [ - { - "start": 7, - "end": 59, - "text": "Свердловская область , Берёзовский , Транспортная 8А" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1012, - "text": "12 Crag South Avenue , Downtown Westside . He laughs , saying both are true . His phone number printed above the bar", - "gold": [ - { - "start": 0, - "end": 40, - "text": "12 Crag South Avenue , Downtown Westside" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1040, - "text": "Панама , Димитровград , 117312 , УЛ . ВАВИЛОВА , д . 19", - "gold": [ - { - "start": 0, - "end": 55, - "text": "Панама , Димитровград , 117312 , УЛ . ВАВИЛОВА , д . 19" - } - ], - "predicted": [ - { - "start": 24, - "end": 55, - "text": "117312 , УЛ . ВАВИЛОВА , д . 19", - "confidence": 0.75, - "signals": [ - "street_marker", - "house_marker", - "postal_code", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 1070, - "text": "Уважаемые господа , направляем вам наше коммерческое предложение на поставку комплектующих . Подробные условия и спецификацию вы найдете в приложении . Для заключения договора просим использовать следующие реквизиты : ООО « Строитель » , ИНН 7707083893 , КПП 770101001 , ОГРН 1027739123456 , адрес : 105064 , г . Невинномысск , ул . Земляной Вал , д . 26 , стр . 2 . Ожидаем вашего ответа .", - "gold": [ - { - "start": 313, - "end": 364, - "text": "Невинномысск , ул . Земляной Вал , д . 26 , стр . 2" - } - ], - "predicted": [ - { - "start": 300, - "end": 364, - "text": "105064 , г . Невинномысск , ул . Земляной Вал , д . 26 , стр . 2", - "confidence": 0.99, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "postal_code", - "location_marker", - "unit_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 1074, - "text": "Поставщик : ООО « Ромашка » , ИНН 7707083893 , юридический адрес : г . Чапаевск , ул . Ленина , д . 1 . Получатель : ООО « Василек » , ИНН 0987654321 , адрес : г . Димитровград , пр . Просвещения , д . 10 . Договор № 55 от 15 . 03 . 2024 .", - "gold": [ - { - "start": 71, - "end": 101, - "text": "Чапаевск , ул . Ленина , д . 1" - }, - { - "start": 164, - "end": 204, - "text": "Димитровград , пр . Просвещения , д . 10" - } - ], - "predicted": [ - { - "start": 67, - "end": 101, - "text": "г . Чапаевск , ул . Ленина , д . 1", - "confidence": 0.91, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "missed_address", - "span_includes_context" - ] - }, - { - "source_row": 1101, - "text": "| Поставщик | Адрес | Email | | ООО Вектор | Тверь , ш . Московское , д . 2 , оф . 310 | timur@dev . global . com |", - "gold": [ - { - "start": 45, - "end": 86, - "text": "Тверь , ш . Московское , д . 2 , оф . 310" - } - ], - "predicted": [ - { - "start": 20, - "end": 86, - "text": "| Email | | ООО Вектор | Тверь , ш . Московское , д . 2 , оф . 310", - "confidence": 0.9, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "unit_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 1114, - "text": "Адрес: г . Невинномысск , ш . Люблинская , дом 177 . Email: natalya_vinogradov@rambler . ru .", - "gold": [ - { - "start": 11, - "end": 50, - "text": "Невинномысск , ш . Люблинская , дом 177" - } - ], - "predicted": [ - { - "start": 7, - "end": 50, - "text": "г . Невинномысск , ш . Люблинская , дом 177", - "confidence": 0.96, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 1148, - "text": "Адрес: г . Чапаевск , просп . Рокоссовского , д . 151 . Сайт: https://spotify . com/terms .", - "gold": [ - { - "start": 11, - "end": 53, - "text": "Чапаевск , просп . Рокоссовского , д . 151" - } - ], - "predicted": [ - { - "start": 7, - "end": 53, - "text": "г . Чапаевск , просп . Рокоссовского , д . 151", - "confidence": 0.91, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 1161, - "text": "ВОЛЬСК , ПРОФСОЮЗНАЯ 15", - "gold": [ - { - "start": 0, - "end": 23, - "text": "ВОЛЬСК , ПРОФСОЮЗНАЯ 15" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1164, - "text": "Настоящим удостоверяю , что Лукин Питер Платонович , проживающий по адресу : г . Нижнем Новгород , ул . Ленина , д . 5 , кв . 10 , паспорт серии 4508 № 123456 , выдан ОВД \" Замоскворечье \" г . Crestville 10 . 05 . 2008 , ИНН 500100732259 , является надлежащим образом уполномоченным представителем по всем вопросам , связанным с управлением недвижимым имуществом , расположенным по адресу : г . Каменск-Уральский , ул . Садовая , д . 1 , к . 2 .", - "gold": [ - { - "start": 81, - "end": 128, - "text": "Нижнем Новгород , ул . Ленина , д . 5 , кв . 10" - }, - { - "start": 395, - "end": 443, - "text": "Каменск-Уральский , ул . Садовая , д . 1 , к . 2" - } - ], - "predicted": [ - { - "start": 77, - "end": 128, - "text": "г . Нижнем Новгород , ул . Ленина , д . 5 , кв . 10", - "confidence": 0.96, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "unit_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - }, - { - "start": 391, - "end": 443, - "text": "г . Каменск-Уральский , ул . Садовая , д . 1 , к . 2", - "confidence": 0.96, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "unit_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 1196, - "text": "Счет № 123 от 15 . 03 . 2023 . Поставщик : ООО \" СтройПрогресс \" , ИНН 7707083893 , КПП 772301001 . Адрес : г . Бузулук , ул . Лесная , д . 5 . Получатель : ЗАО \" Альфа \" , ИНН 7734567890 , КПП 773401001 . Адрес : г . Невинномысск , ул . Тверская , д . 10 . Сумма к оплате : 50 000 руб . Назначение платежа : Оплата по договору № 45 от 01 . 03 . 2023 .", - "gold": [ - { - "start": 112, - "end": 141, - "text": "Бузулук , ул . Лесная , д . 5" - }, - { - "start": 218, - "end": 255, - "text": "Невинномысск , ул . Тверская , д . 10" - } - ], - "predicted": [ - { - "start": 108, - "end": 141, - "text": "г . Бузулук , ул . Лесная , д . 5", - "confidence": 0.91, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - }, - { - "start": 214, - "end": 255, - "text": "г . Невинномысск , ул . Тверская , д . 10", - "confidence": 0.91, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 1206, - "text": "Договор № 123 / 2023 от 15 . 03 . 2023 . Поставщик : ООО « Ромашка » , ИНН 7707083893 , адрес : г . Kronshtadt , ул . Лесная , д . 5 . Покупатель : ИП Филат И . И . , ИНН 5001122334 , адрес : г . Обнински , ул . Мира , д . 10 . Настоящий договор составлен в двух экземплярах , имеющих одинаковую юридическую силу .", - "gold": [ - { - "start": 100, - "end": 132, - "text": "Kronshtadt , ул . Лесная , д . 5" - }, - { - "start": 196, - "end": 225, - "text": "Обнински , ул . Мира , д . 10" - } - ], - "predicted": [ - { - "start": 96, - "end": 132, - "text": "г . Kronshtadt , ул . Лесная , д . 5", - "confidence": 0.91, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - }, - { - "start": 192, - "end": 225, - "text": "г . Обнински , ул . Мира , д . 10", - "confidence": 0.91, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 1209, - "text": "Vernon живЕТ на улице ЛЕНинГрадской , в городе Ейск , в квартире № 12", - "gold": [ - { - "start": 16, - "end": 69, - "text": "улице ЛЕНинГрадской , в городе Ейск , в квартире № 12" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1216, - "text": "Нижний Новгород , пр . Хавская , 88 — 8-831-422-50-60", - "gold": [ - { - "start": 0, - "end": 35, - "text": "Нижний Новгород , пр . Хавская , 88" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1219, - "text": "Автосервис Автопилот на метро Войковская Димитровград , Старопетровский проезд , 9А Округ САО , Нижегородский Войковский ( - )", - "gold": [ - { - "start": 41, - "end": 120, - "text": "Димитровград , Старопетровский проезд , 9А Округ САО , Нижегородский Войковский" - } - ], - "predicted": [ - { - "start": 56, - "end": 83, - "text": "Старопетровский проезд , 9А", - "confidence": 0.44, - "signals": [ - "street_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 1295, - "text": "Адрес доставки: Новошахтинск ул . Нижегородская 9 , кв . 11", - "gold": [ - { - "start": 16, - "end": 59, - "text": "Новошахтинск ул . Нижегородская 9 , кв . 11" - } - ], - "predicted": [ - { - "start": 29, - "end": 59, - "text": "ул . Нижегородская 9 , кв . 11", - "confidence": 0.67, - "signals": [ - "address_cue", - "street_marker", - "unit_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 1310, - "text": "пр . Нижегородская , 12А , +44-8133-836688 Онлайн-запись на https://new . cian . ru/catalog . Cluster node: 172 . 22 . 197 . 235 .", - "gold": [ - { - "start": 0, - "end": 24, - "text": "пр . Нижегородская , 12А" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1339, - "text": "ООО \" Русские технологии \" 127051 , Регион 77 , ГлАзОВ , улица петровка , дом 38 , строение 1 ИНН 7706543210 Место расчётов: russian-technologies . ru", - "gold": [ - { - "start": 48, - "end": 93, - "text": "ГлАзОВ , улица петровка , дом 38 , строение 1" - } - ], - "predicted": [ - { - "start": 57, - "end": 93, - "text": "улица петровка , дом 38 , строение 1", - "confidence": 0.77, - "signals": [ - "street_marker", - "house_marker", - "unit_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 1346, - "text": "MOY ADRES PROPISKI: ЭСТОНИЯ , AKMOLINSKAYA OBLAST G . ASTANA UL . DOSTYK D . 5 KV . 1", - "gold": [ - { - "start": 20, - "end": 85, - "text": "ЭСТОНИЯ , AKMOLINSKAYA OBLAST G . ASTANA UL . DOSTYK D . 5 KV . 1" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1364, - "text": "Приёмная , пр . Генерала Карбышева , д . 7 , кв . 88 . 8 812 682 53 49 .", - "gold": [ - { - "start": 11, - "end": 52, - "text": "пр . Генерала Карбышева , д . 7 , кв . 88" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1420, - "text": "напиши подробный план равизтия учебного центра английского языка - помещение 2 класса , хол , первый этаж , парковка Бритландия Трофимова 7 А", - "gold": [ - { - "start": 128, - "end": 141, - "text": "Трофимова 7 А" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1450, - "text": "MOY ADRES PROPISKI: МАДАГАСКАР , KIEVSKAYA OBLAST G . IRPIN UL . KIEVSKAYA D . 20 KV . 2", - "gold": [ - { - "start": 20, - "end": 88, - "text": "МАДАГАСКАР , KIEVSKAYA OBLAST G . IRPIN UL . KIEVSKAYA D . 20 KV . 2" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1489, - "text": "ООО \" Mail . ru Group \" 125167 , Алатырь , Ленинградский проспект , дом 39 , строение 79 ИНН 7719488300 Место расчётов: mail . ru", - "gold": [ - { - "start": 33, - "end": 88, - "text": "Алатырь , Ленинградский проспект , дом 39 , строение 79" - } - ], - "predicted": [ - { - "start": 43, - "end": 88, - "text": "Ленинградский проспект , дом 39 , строение 79", - "confidence": 0.77, - "signals": [ - "street_marker", - "house_marker", - "unit_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 1504, - "text": "Адрес доставки: бузулук ул . Краснопролетарская 34 , кв . 78", - "gold": [ - { - "start": 16, - "end": 60, - "text": "бузулук ул . Краснопролетарская 34 , кв . 78" - } - ], - "predicted": [ - { - "start": 24, - "end": 60, - "text": "ул . Краснопролетарская 34 , кв . 78", - "confidence": 0.67, - "signals": [ - "address_cue", - "street_marker", - "unit_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 1578, - "text": "Для формирования счета на оплату , пожалуйста , предоставьте полный перечень реквизитов вашей организации . Идентификационный номер налогоплательщика : 7707083893 . Адрес доставки : г . Краснодар , Невский пр . , д . 10 .", - "gold": [ - { - "start": 186, - "end": 219, - "text": "Краснодар , Невский пр . , д . 10" - } - ], - "predicted": [ - { - "start": 182, - "end": 219, - "text": "г . Краснодар , Невский пр . , д . 10", - "confidence": 0.63, - "signals": [ - "address_cue", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 1596, - "text": "Склад: Промышленная ул . , стр . 7 , +7 926 301-44-58", - "gold": [ - { - "start": 7, - "end": 34, - "text": "Промышленная ул . , стр . 7" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1602, - "text": "SAMARA , LENINA 25", - "gold": [ - { - "start": 0, - "end": 18, - "text": "SAMARA , LENINA 25" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1614, - "text": "Apple Inc . 1 Infinite Loop , cupertino , ОРЛОВСКАЯ 95014 БОТСВАНА", - "gold": [ - { - "start": 12, - "end": 66, - "text": "1 Infinite Loop , cupertino , ОРЛОВСКАЯ 95014 БОТСВАНА" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1628, - "text": "АО « СБЕРБАНК катар » УЛ . БОЛЬШАЯ ДМИТРОВКА , 32 , СТР . 1 , УСОЛЬЕ-СИБИРСКОЕ , 107031 , ГРЕНАДА ТЕЛ . : +1-244-518-3825 , WWW . SBERBANK . RU", - "gold": [ - { - "start": 14, - "end": 97, - "text": "катар » УЛ . БОЛЬШАЯ ДМИТРОВКА , 32 , СТР . 1 , УСОЛЬЕ-СИБИРСКОЕ , 107031 , ГРЕНАДА" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1636, - "text": "Как добраться до пр . Толстого , д . 176 в Люберцы ?", - "gold": [ - { - "start": 17, - "end": 50, - "text": "пр . Толстого , д . 176 в Люберцы" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1668, - "text": "recipient_fio = Калашникова Инна recipient_addr = Вологда , ул . Герцена , дом 11 recipient_email = i . kalashnikova@vologda . ru", - "gold": [ - { - "start": 50, - "end": 81, - "text": "Вологда , ул . Герцена , дом 11" - } - ], - "predicted": [ - { - "start": 60, - "end": 81, - "text": "ул . Герцена , дом 11", - "confidence": 0.72, - "signals": [ - "street_marker", - "house_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 1693, - "text": "Ближайшее отделение: бул . Академика Королёва , д . 78 , кв . 7 . Почта: maksim@bedrijf . nl .", - "gold": [ - { - "start": 21, - "end": 63, - "text": "бул . Академика Королёва , д . 78 , кв . 7" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1739, - "text": "MOY ADRES PROPISKI: ROSSIYA , КИРОВСКАЯ OBLAST G . MOSKVA UL . ARBAT D . 15 KV . 50", - "gold": [ - { - "start": 20, - "end": 83, - "text": "ROSSIYA , КИРОВСКАЯ OBLAST G . MOSKVA UL . ARBAT D . 15 KV . 50" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1777, - "text": "Счет № 123 от 15 . 03 . 2024 Поставщик : ООО \" Вектор \" , ИНН 7707083893 , Юридический адрес : 190000 , г . ASTANA , ул . Невская , д . 5 . Покупатель : ЗАО \" Горизонт \" , ИНН : 4321098765 . Адрес : 125009 , г . Пермь , ул . Тверская , д . 10 .", - "gold": [ - { - "start": 108, - "end": 137, - "text": "ASTANA , ул . Невская , д . 5" - }, - { - "start": 212, - "end": 242, - "text": "Пермь , ул . Тверская , д . 10" - } - ], - "predicted": [ - { - "start": 95, - "end": 137, - "text": "190000 , г . ASTANA , ул . Невская , д . 5", - "confidence": 0.99, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "postal_code", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - }, - { - "start": 199, - "end": 242, - "text": "125009 , г . Пермь , ул . Тверская , д . 10", - "confidence": 0.99, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "postal_code", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 1883, - "text": "Завещание . Я , Торопин Ермолай Германович , residing at : ромнах , Tverskaya St . 20 , apt . 5 , my INN is 500100732259 , do hereby declare my last will and testament .", - "gold": [ - { - "start": 59, - "end": 95, - "text": "ромнах , Tverskaya St . 20 , apt . 5" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 1947, - "text": "Глазов , 117997 , ул . Вавилова , д . 19", - "gold": [ - { - "start": 0, - "end": 40, - "text": "Глазов , 117997 , ул . Вавилова , д . 19" - } - ], - "predicted": [ - { - "start": 9, - "end": 40, - "text": "117997 , ул . Вавилова , д . 19", - "confidence": 0.75, - "signals": [ - "street_marker", - "house_marker", - "postal_code", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 1951, - "text": "Здравствуйте , хочу заказать товары из вашего интернет-магазина . Доставка в город аЛАТЫРЬ , улица Советская , дом 8 , квартира 74 .", - "gold": [ - { - "start": 83, - "end": 130, - "text": "аЛАТЫРЬ , улица Советская , дом 8 , квартира 74" - } - ], - "predicted": [ - { - "start": 66, - "end": 130, - "text": "Доставка в город аЛАТЫРЬ , улица Советская , дом 8 , квартира 74", - "confidence": 0.78, - "signals": [ - "street_marker", - "house_marker", - "location_marker", - "unit_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 1978, - "text": "MOY ADRES PROPISKI: АБХАЗИЯ , GRODNENSKAYA OBLAST G . GRODNO Цветной бульвар D . 5 KV . 12", - "gold": [ - { - "start": 20, - "end": 90, - "text": "АБХАЗИЯ , GRODNENSKAYA OBLAST G . GRODNO Цветной бульвар D . 5 KV . 12" - } - ], - "predicted": [ - { - "start": 69, - "end": 82, - "text": "бульвар D . 5", - "confidence": 0.44, - "signals": [ - "street_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 1982, - "text": "Переехали на Комсомольский проспект , дом 34 кв . 118", - "gold": [ - { - "start": 13, - "end": 53, - "text": "Комсомольский проспект , дом 34 кв . 118" - } - ], - "predicted": [ - { - "start": 0, - "end": 53, - "text": "Переехали на Комсомольский проспект , дом 34 кв . 118", - "confidence": 0.72, - "signals": [ - "street_marker", - "house_marker", - "unit_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 2069, - "text": "Автосервис КАПИТАН-АВТО на метро Коптево Вольск , КРОНШТАДТСКИЙ БУЛЬВАР , 35б Округ САО , Люблино Головинский ( доехать , узнать ) ( ? ? ? )", - "gold": [ - { - "start": 27, - "end": 109, - "text": "метро Коптево Вольск , КРОНШТАДТСКИЙ БУЛЬВАР , 35б Округ САО , Люблино Головинский" - } - ], - "predicted": [ - { - "start": 50, - "end": 77, - "text": "КРОНШТАДТСКИЙ БУЛЬВАР , 35б", - "confidence": 0.44, - "signals": [ - "street_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 2074, - "text": "Никитский бул . , 12", - "gold": [ - { - "start": 0, - "end": 20, - "text": "Никитский бул . , 12" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 2078, - "text": "живу г . Северодвинск ул . блока д . 65 не далеко от центра", - "gold": [ - { - "start": 9, - "end": 39, - "text": "Северодвинск ул . блока д . 65" - } - ], - "predicted": [ - { - "start": 22, - "end": 39, - "text": "ул . блока д . 65", - "confidence": 0.67, - "signals": [ - "street_marker", - "house_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 2104, - "text": "Магазин на ПР . ТИМИРЯЗЕВСКАЯ , дом 110 , кв . 219 , заказы на s . komarov@bmstu . ru .", - "gold": [ - { - "start": 11, - "end": 50, - "text": "ПР . ТИМИРЯЗЕВСКАЯ , дом 110 , кв . 219" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 2108, - "text": "Лев — Северодвинск ул . Толстого , дом 96", - "gold": [ - { - "start": 6, - "end": 41, - "text": "Северодвинск ул . Толстого , дом 96" - } - ], - "predicted": [ - { - "start": 19, - "end": 41, - "text": "ул . Толстого , дом 96", - "confidence": 0.72, - "signals": [ - "street_marker", - "house_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 2113, - "text": " Сербия Ленинградская обл . Всеволожский район наб . Фонтанки д . 30 ", - "gold": [ - { - "start": 21, - "end": 168, - "text": "Сербия Ленинградская обл . Всеволожский район наб . Фонтанки д . 30" - } - ], - "predicted": [ - { - "start": 129, - "end": 168, - "text": "наб . Фонтанки д . 30", - "confidence": 0.67, - "signals": [ - "street_marker", - "house_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 2115, - "text": "КОМПАНИЯ ООО « RUTUBE » АДРЕС ГОРОД НОВОШАХТИНСК УЛИЦА ДАНИЛОВСКАЯ ДОМ 10 КОРПУС 1", - "gold": [ - { - "start": 36, - "end": 82, - "text": "НОВОШАХТИНСК УЛИЦА ДАНИЛОВСКАЯ ДОМ 10 КОРПУС 1" - } - ], - "predicted": [ - { - "start": 30, - "end": 82, - "text": "ГОРОД НОВОШАХТИНСК УЛИЦА ДАНИЛОВСКАЯ ДОМ 10 КОРПУС 1", - "confidence": 0.99, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "unit_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 2142, - "text": "Адрес офиса: пр . Обручева , д . 1 . Email: tarasov . ivan@rambler . ru .", - "gold": [ - { - "start": 13, - "end": 34, - "text": "пр . Обручева , д . 1" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 2185, - "text": "Библиотека в выхинО-ЖуЛЕБИНО районе , пр . Электрозаводская , д . 58 , кв . 198 , email vitali1975@rambler . ru .", - "gold": [ - { - "start": 13, - "end": 79, - "text": "выхинО-ЖуЛЕБИНО районе , пр . Электрозаводская , д . 58 , кв . 198" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 2216, - "text": "Адрес доставки: Ейск ул . Вавилова 56 , кв . 3", - "gold": [ - { - "start": 16, - "end": 46, - "text": "Ейск ул . Вавилова 56 , кв . 3" - } - ], - "predicted": [ - { - "start": 21, - "end": 46, - "text": "ул . Вавилова 56 , кв . 3", - "confidence": 0.67, - "signals": [ - "address_cue", - "street_marker", - "unit_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 2229, - "text": "К проекту присоединился и кронштадт . У Морского собора по адресу Якорная площадь , дом 1 , оборудовали специальный павильон , в котором показывали карты островов Финского залива . Но на одном из стендов адрес значился : “ Якроная пл . 1 , Kronshtadt City” , и организаторы уверяли , что такие ошибки демонстрируют живую природу города .", - "gold": [ - { - "start": 66, - "end": 89, - "text": "Якорная площадь , дом 1" - }, - { - "start": 223, - "end": 250, - "text": "Якроная пл . 1 , Kronshtadt" - } - ], - "predicted": [ - { - "start": 66, - "end": 89, - "text": "Якорная площадь , дом 1", - "confidence": 0.9, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "parsed_street", - "parsed_house" - ] - }, - { - "start": 210, - "end": 237, - "text": "значился : “ Якроная пл . 1", - "confidence": 0.62, - "signals": [ - "address_cue", - "street_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text", - "span_includes_context" - ] - }, - { - "source_row": 2255, - "text": "Производственная компания « Альфа » приглашает к сотрудничеству дилеров . Мы предлагаем выгодные условия партнерства и поддержку на всех этапах . Подробности по телефону или по электронной почте . Адрес для корреспонденции : MOSKVA , ул . Центральная , д . 1 . Наш инн 7707083893 .", - "gold": [ - { - "start": 225, - "end": 258, - "text": "MOSKVA , ул . Центральная , д . 1" - } - ], - "predicted": [ - { - "start": 203, - "end": 258, - "text": "для корреспонденции : MOSKVA , ул . Центральная , д . 1", - "confidence": 0.85, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 2269, - "text": "Поставщик : ООО \" Лучшее Решение \" , ИНН : 7707083893 , юридический адрес : г . Бузулук , ул . Примерная , д . 1 . Предлагаем к поставке партию строительных материалов согласно приложенной спецификации . Готовы предоставить все необходимые документы .", - "gold": [ - { - "start": 80, - "end": 112, - "text": "Бузулук , ул . Примерная , д . 1" - } - ], - "predicted": [ - { - "start": 76, - "end": 112, - "text": "г . Бузулук , ул . Примерная , д . 1", - "confidence": 0.91, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 2285, - "text": "Поставщик : ООО « Альфа » ( ИНН 7707083893 ) , юридический адрес : 105082 , г . Каменск-Уральский , ул . Бакунинская , д . 5 . Договор № 123 / 2023 от 15 . 03 . 2023 .", - "gold": [ - { - "start": 80, - "end": 124, - "text": "Каменск-Уральский , ул . Бакунинская , д . 5" - } - ], - "predicted": [ - { - "start": 67, - "end": 124, - "text": "105082 , г . Каменск-Уральский , ул . Бакунинская , д . 5", - "confidence": 0.99, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "postal_code", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 2316, - "text": "Самовывоз: Димитровград , ул . Малышева , д . 51 , +7 ( 343 ) 200-10-20", - "gold": [ - { - "start": 11, - "end": 48, - "text": "Димитровград , ул . Малышева , д . 51" - } - ], - "predicted": [ - { - "start": 26, - "end": 48, - "text": "ул . Малышева , д . 51", - "confidence": 0.67, - "signals": [ - "street_marker", - "house_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 2329, - "text": "Адрес доставки: АЛАТЫРЬ ул . Светланская 15 , офис 207", - "gold": [ - { - "start": 16, - "end": 54, - "text": "АЛАТЫРЬ ул . Светланская 15 , офис 207" - } - ], - "predicted": [ - { - "start": 24, - "end": 54, - "text": "ул . Светланская 15 , офис 207", - "confidence": 0.67, - "signals": [ - "address_cue", - "street_marker", - "unit_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 2332, - "text": "Отделение в посёлке Малаховка , Быковское шоссе д . 9", - "gold": [ - { - "start": 20, - "end": 53, - "text": "Малаховка , Быковское шоссе д . 9" - } - ], - "predicted": [ - { - "start": 32, - "end": 53, - "text": "Быковское шоссе д . 9", - "confidence": 0.67, - "signals": [ - "street_marker", - "house_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 2336, - "text": "+7 985 469-24-69 , Зеленодольск , московское шоссе , д . 15 Прайс-лист на https://admin . arxiv . org/products . Connected via 192 . 168 . 248 . 29 .", - "gold": [ - { - "start": 19, - "end": 59, - "text": "Зеленодольск , московское шоссе , д . 15" - } - ], - "predicted": [ - { - "start": 34, - "end": 59, - "text": "московское шоссе , д . 15", - "confidence": 0.67, - "signals": [ - "street_marker", - "house_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 2351, - "text": "MOY ADRES PROPISKI: ЧЕРНОГОРИЯ , ANDIZHANSKAYA OBLAST G . ANDIZHAN ul . navroz D . 10 KV . 6", - "gold": [ - { - "start": 20, - "end": 92, - "text": "ЧЕРНОГОРИЯ , ANDIZHANSKAYA OBLAST G . ANDIZHAN ul . navroz D . 10 KV . 6" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 2370, - "text": "В связи с ненадлежащим исполнением условий договора № 123 от 15 . 01 . 2023 , прошу Вас в срок до 01 . 03 . 2024 направить в наш адрес письменные объяснения . Адрес для направления документов : 101000 , Рыбинск , ул . Примерная , д . 1 , офис 5 . Реквизиты контрагента : ООО \" СтройДом \" , ИНН 7707083893 , КПП 667890123 .", - "gold": [ - { - "start": 203, - "end": 244, - "text": "Рыбинск , ул . Примерная , д . 1 , офис 5" - } - ], - "predicted": [ - { - "start": 165, - "end": 244, - "text": "для направления документов : 101000 , Рыбинск , ул . Примерная , д . 1 , офис 5", - "confidence": 0.98, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "postal_code", - "unit_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 2372, - "text": "сервак 23 . 233 . 99 . 78:22 лежит опять . Заявитель: Ingram Клим Макарович , г . Бузулук , бул . Цветной бульвар , д . 98 , кв . 120 , подробнее на https://ranepa . ru/help .", - "gold": [ - { - "start": 82, - "end": 133, - "text": "Бузулук , бул . Цветной бульвар , д . 98 , кв . 120" - } - ], - "predicted": [ - { - "start": 98, - "end": 133, - "text": "Цветной бульвар , д . 98 , кв . 120", - "confidence": 0.72, - "signals": [ - "street_marker", - "house_marker", - "unit_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 2380, - "text": "MOY ADRES PROPISKI: НЕПАЛ , CHUYSKAYA OBLAST G . BISHKEK академика королёва D . 10 KV . 15", - "gold": [ - { - "start": 20, - "end": 90, - "text": "НЕПАЛ , CHUYSKAYA OBLAST G . BISHKEK академика королёва D . 10 KV . 15" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 2382, - "text": "Вниманию ООО \" СтройКомПлюс \" , расположенного по адресу : г . Тверь , ул . Строителей , д . 15 , ИНН 7707083893 , сообщаем о нарушении сроков поставки строительных материалов . В соответствии с пунктом 3 . 2 Договора № 145 от 10 . 01 . 2023 , поставка должна была быть осуществлена до 01 . 03 . 2023 . На сегодняшний день материалы не поступили , что создает существенные препятствия для выполнения наших обязательств перед заказчиком .", - "gold": [ - { - "start": 63, - "end": 95, - "text": "Тверь , ул . Строителей , д . 15" - } - ], - "predicted": [ - { - "start": 59, - "end": 112, - "text": "г . Тверь , ул . Строителей , д . 15 , ИНН 7707083893", - "confidence": 0.91, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 2389, - "text": "Зарегистрирован: г . Северодвинск , калмыкии обл . , ул . толстого , д . 103", - "gold": [ - { - "start": 21, - "end": 76, - "text": "Северодвинск , калмыкии обл . , ул . толстого , д . 103" - } - ], - "predicted": [ - { - "start": 17, - "end": 76, - "text": "г . Северодвинск , калмыкии обл . , ул . толстого , д . 103", - "confidence": 0.73, - "signals": [ - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 2407, - "text": "При заключении кредитного договора № 456789012345 , прошу указать следующие данные : Шувалов Платон Артёмович , ИНН 500100732259 , дата рождения 01 . 01 . 1990 . Проживает по адресу : г . Алатырь , ул . Ленина , д . 1 , кв . 1 .", - "gold": [ - { - "start": 188, - "end": 226, - "text": "Алатырь , ул . Ленина , д . 1 , кв . 1" - } - ], - "predicted": [ - { - "start": 184, - "end": 226, - "text": "г . Алатырь , ул . Ленина , д . 1 , кв . 1", - "confidence": 0.96, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "unit_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 2411, - "text": "Локации включали не только улицы и проспекты , но и метки : “центр , около ТЦ Gulliver” , “рядом с метро Дворец Украина” , ул Обручева , 16 , Люблинская , 10 , ул Усачёва , 44А , УЛ БАУМАНСКАЯ , 34 , УЛ ЯРЦЕВСКАЯ , 12 . Иногда встречались устаревшие адреса , записанные как “ ул . Ленина , 5 , Киев ” или “г . ейск , центр , напротив театра” .", - "gold": [ - { - "start": 123, - "end": 217, - "text": "ул Обручева , 16 , Люблинская , 10 , ул Усачёва , 44А , УЛ БАУМАНСКАЯ , 34 , УЛ ЯРЦЕВСКАЯ , 12" - }, - { - "start": 276, - "end": 298, - "text": "ул . Ленина , 5 , Киев" - } - ], - "predicted": [ - { - "start": 160, - "end": 176, - "text": "ул Усачёва , 44А", - "confidence": 0.44, - "signals": [ - "street_marker", - "parsed_street", - "parsed_house" - ] - }, - { - "start": 179, - "end": 197, - "text": "УЛ БАУМАНСКАЯ , 34", - "confidence": 0.44, - "signals": [ - "street_marker", - "parsed_street", - "parsed_house" - ] - }, - { - "start": 200, - "end": 217, - "text": "УЛ ЯРЦЕВСКАЯ , 12", - "confidence": 0.44, - "signals": [ - "street_marker", - "parsed_street", - "parsed_house" - ] - }, - { - "start": 276, - "end": 291, - "text": "ул . Ленина , 5", - "confidence": 0.62, - "signals": [ - "address_cue", - "street_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text", - "spurious_address" - ] - }, - { - "source_row": 2425, - "text": "Бампер передний в цвет Ford Focus 3 ВОЛЬСК , ул . Плеханова , 10Ас3", - "gold": [ - { - "start": 36, - "end": 67, - "text": "ВОЛЬСК , ул . Плеханова , 10Ас3" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 2451, - "text": "К заявке на участие в тендере прилагаем следующую информацию : Общество с ограниченной ответственностью \" Альфа \" , ИНН 7707083893 , юридический адрес : г . Нижнем Новгород , ул . Ленина , д . 1 . Готовы предоставить дополнительные документы по запросу .", - "gold": [ - { - "start": 157, - "end": 194, - "text": "Нижнем Новгород , ул . Ленина , д . 1" - } - ], - "predicted": [ - { - "start": 153, - "end": 194, - "text": "г . Нижнем Новгород , ул . Ленина , д . 1", - "confidence": 0.91, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 2466, - "text": "Адрес доставки: Усолье-Сибирское ул . социалистическая 7 , кв . 12", - "gold": [ - { - "start": 16, - "end": 66, - "text": "Усолье-Сибирское ул . социалистическая 7 , кв . 12" - } - ], - "predicted": [ - { - "start": 33, - "end": 66, - "text": "ул . социалистическая 7 , кв . 12", - "confidence": 0.67, - "signals": [ - "address_cue", - "street_marker", - "unit_marker", - "parsed_street", - "parsed_house" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 2514, - "text": "Бузулук , ул . Кольцовская , д . 9 , +7 ( 951 ) 270 63 31", - "gold": [ - { - "start": 0, - "end": 34, - "text": "Бузулук , ул . Кольцовская , д . 9" - } - ], - "predicted": [ - { - "start": 10, - "end": 34, - "text": "ул . Кольцовская , д . 9", - "confidence": 0.67, - "signals": [ - "street_marker", - "house_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 2531, - "text": "MOY ADRES PROPISKI: КАТАР , KIEVSKAYA OBLAST G . ГЛАЗОВ UL . KRESHCHATIK D . 10 KV . 36", - "gold": [ - { - "start": 20, - "end": 87, - "text": "КАТАР , KIEVSKAYA OBLAST G . ГЛАЗОВ UL . KRESHCHATIK D . 10 KV . 36" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 2589, - "text": "src= 208 . 222 . 5 . 31:53 dst= 10 . 0 . 0 . 1 . В ромнах , на улице Гетьмана Мазепи 26 , международным письмам прописывают адрес иначе : Hetman Mazepa street , Romny , ГРЕНАДА , но индекс иногда теряется .", - "gold": [ - { - "start": 51, - "end": 87, - "text": "ромнах , на улице Гетьмана Мазепи 26" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 2600, - "text": "8 919 431-42-62 , адрес: пр . ферганская , д . 130 , кв . 220 .", - "gold": [ - { - "start": 25, - "end": 61, - "text": "пр . ферганская , д . 130 , кв . 220" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 2648, - "text": "City maps change faster than memories . A couple once met every evening at “Old Lantern Café , 52A Howard St . , Crestville , ” but the address updated to 52 Howard Street , and then the café moved entirely to 18 Seaview Drive with a bright phone number +353 85 224 9087 printed on menus . The original lot now displays a grocery’s poorly translated name “Lanterno Shop Market , ” leaving locals nostalgic for the old wooden chairs and the window facing the coordinates 53 . 3498 , -6 . 2603 which used to align perfectly with the sunset . When I visited on 07/14/2022 , the new waitress said the café is still the heart of the city but the heart beats in different ribs .", - "gold": [ - { - "start": 95, - "end": 123, - "text": "52A Howard St . , Crestville" - }, - { - "start": 155, - "end": 171, - "text": "52 Howard Street" - }, - { - "start": 210, - "end": 226, - "text": "18 Seaview Drive" - } - ], - "predicted": [], - "failure_reasons": [ - "missed_address" - ] - }, - { - "source_row": 2698, - "text": "Гражданин Тихонов Лев Савельевич , 15 . 05 . 1985 г . р . , ИНН 500100732259 , проживающий по адресу : г . Romny , ул . Ленина , д . 5 , кв . 10 . Прошу предоставить справку о состоянии лицевого счета .", - "gold": [ - { - "start": 107, - "end": 144, - "text": "Romny , ул . Ленина , д . 5 , кв . 10" - } - ], - "predicted": [ - { - "start": 103, - "end": 144, - "text": "г . Romny , ул . Ленина , д . 5 , кв . 10", - "confidence": 0.96, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "unit_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 2707, - "text": "Автосервис : Алатырь , Ул . Дубнинская , д . 83 , стр . 5 ( 03 . 06 с 10 : 00 ) - 12000 руб работа под ключ + стоимость деталей ( ~ 15000 - 16000 руб )", - "gold": [ - { - "start": 13, - "end": 57, - "text": "Алатырь , Ул . Дубнинская , д . 83 , стр . 5" - } - ], - "predicted": [ - { - "start": 23, - "end": 57, - "text": "Ул . Дубнинская , д . 83 , стр . 5", - "confidence": 0.72, - "signals": [ - "street_marker", - "house_marker", - "unit_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_drops_gold_text" - ] - }, - { - "source_row": 2764, - "text": "Центр оказывает посильную помощь семьям , воспитывающим несовершеннолетних детей , инвалидам , одиноким пожилым гражданам . На сайте Вы найдете информацию о реализации различных программ , работе клубов , режиме работы учреждения , деятельности структурных подразделений , оказываемых социальных услугах . Вы можете обратиться к специалистам учреждения через интернет , отправить письмо на нашу электронную почту belkids @ beladm . ru . Важная информация для семей с детьми до 1 , 5 лет Уважаемые родители ! Информируем вас о возможности получения необходимых детских вещей во временное пользование в рамках социального проек . . . . . . ели для близнецов )  Прогулочная коляска  Стул для кормления  Автолюлька для новорожденного  Автокресло для детей от 6 месяцев Кто может воспользоваться услугой :  Семьи , где родители учатся очно  Многодетные семьи  Неполные семьи и одинокие мамы  Семьи с детьми - инвалидами  Малообеспеченные семьи и семьи в трудной жизненной ситуации Условия предоставления :  Срок пользования согласовывается индивидуально  Все предметы предоставляются временно Где получить помощь :  Отделение « Семейный МФЦ » МБУ « Комплексный центр социального обслуживания населения города Белгорода »  Адрес : г . Белгород , ул . Королева , д . 9  Телефон : + 7 ( 4722 ) 55 - 15 - 75 , + 7 ( 4722 ) 52 - 58 - 13  Режим работы : пн - пт с 9 : 00 до 18 : 00 ( перерыв 13 : 00 - 14 : 00 ) Дополнительные услуги центра  Бесплатное юридическое сопровождение  Психологическая поддержка  Педагогическая помощь  Социальное консультирование  Помощь в получении мер поддержки  Организация досуга На территории Белгородской области запущена работа мобильного приложения « Куратор семьи » , предназначенного для семей участников СВО и для семей граждан пострадавших в результате обстрелов со стороны ВСУ . Не упустите возможность получить необходимую поддержку для вашей семьи ! Важно : все услуги предоставляются бесплатно в режиме « одного окна » . напиши выступление для родительского собрания кратко", - "gold": [ - { - "start": 1242, - "end": 1274, - "text": "Белгород , ул . Королева , д . 9" - } - ], - "predicted": [ - { - "start": 1238, - "end": 1413, - "text": "г . Белгород , ул . Королева , д . 9  Телефон : + 7 ( 4722 ) 55 - 15 - 75 , + 7 ( 4722 ) 52 - 58 - 13  Режим работы : пн - пт с 9 : 00 до 18 : 00 ( перерыв 13 : 00 - 14 : 00", - "confidence": 0.91, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - }, - { - "source_row": 2815, - "text": "Реквизиты для оплаты : ООО « Альфа » , ИНН : 7707083893 . Адрес : г . New Springs , ул . Ленина , д . 1 .", - "gold": [ - { - "start": 70, - "end": 103, - "text": "New Springs , ул . Ленина , д . 1" - } - ], - "predicted": [ - { - "start": 66, - "end": 103, - "text": "г . New Springs , ул . Ленина , д . 1", - "confidence": 0.91, - "signals": [ - "address_cue", - "street_marker", - "house_marker", - "location_marker", - "parsed_street", - "parsed_house", - "unparsed_text" - ] - } - ], - "failure_reasons": [ - "span_includes_context" - ] - } - ], - "source": { - "dataset": "redmadrobot-rnd/pii_benchmark", - "revision": "f77ea831274daf980cc45c61a93c226be9d978d6", - "sha256": "6bf544a380a3ee5bec94b946124bea3afaecce49e734679ad0f0c0e7c12977bb" - } -} diff --git a/evaluation/redmadrobot_report.json b/evaluation/redmadrobot_report.json deleted file mode 100644 index 7b8052a..0000000 --- a/evaluation/redmadrobot_report.json +++ /dev/null @@ -1,1528 +0,0 @@ -{ - "scope": "independent, untuned external evaluation on address snippets from the RedMadRobot Russian PII NER benchmark", - "source": { - "repository": "redmadrobot-rnd/pii_benchmark", - "license": "MIT", - "revision": "f77ea831274daf980cc45c61a93c226be9d978d6", - "sha256": "6bf544a380a3ee5bec94b946124bea3afaecce49e734679ad0f0c0e7c12977bb", - "url": "https://huggingface.co/datasets/redmadrobot-rnd/pii_benchmark/resolve/f77ea831274daf980cc45c61a93c226be9d978d6/test.csv", - "limitations": "production-log-shaped and manually annotated, with real personal values replaced; includes synthetic document-style examples and hard negatives" - }, - "windowing": { - "max_non_location_tokens_between_spans": 3, - "country_is_context_only": true - }, - "source_rows": 493, - "address_snippets": 578, - "matching": "one-to-one same-label span overlap; address windows are oracle-cropped from the benchmark's BIO annotations", - "metric_definitions": { - "span_overlap_micro": "micro precision, recall, and F1 for one-to-one same-label spans with any character overlap", - "exact_span_recall": "exact-boundary same-label matches divided by gold span count", - "fields": "per-field span-overlap precision, recall, and F1" - }, - "span_overlap_micro": { - "tp": 581, - "fp": 387, - "fn": 429, - "support": 1010, - "precision": 0.600207, - "recall": 0.575248, - "f1": 0.587462 - }, - "micro": { - "tp": 581, - "fp": 387, - "fn": 429, - "support": 1010, - "precision": 0.600207, - "recall": 0.575248, - "f1": 0.587462 - }, - "macro_field_f1": 0.592198, - "overlap_matches": 581, - "exact_span_matches": 288, - "exact_span_recall": 0.285149, - "fields": { - "REGION": { - "tp": 66, - "fp": 10, - "fn": 110, - "support": 176, - "precision": 0.868421, - "recall": 0.375, - "f1": 0.52381 - }, - "DISTRICT": { - "tp": 47, - "fp": 3, - "fn": 114, - "support": 161, - "precision": 0.94, - "recall": 0.291925, - "f1": 0.445498 - }, - "CITY": { - "tp": 195, - "fp": 118, - "fn": 144, - "support": 339, - "precision": 0.623003, - "recall": 0.575221, - "f1": 0.59816 - }, - "STREET": { - "tp": 139, - "fp": 256, - "fn": 32, - "support": 171, - "precision": 0.351899, - "recall": 0.812865, - "f1": 0.491166 - }, - "HOUSE": { - "tp": 134, - "fp": 0, - "fn": 29, - "support": 163, - "precision": 1.0, - "recall": 0.822086, - "f1": 0.902357 - } - }, - "failure_sample": [ - { - "source_row": 1, - "text": "Курганской области", - "gold": [ - { - "label": "REGION", - "start": 0, - "end": 18 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 18 - } - ] - }, - { - "source_row": 8, - "text": "Ненецком крае в Забайкальском крае", - "gold": [ - { - "label": "REGION", - "start": 0, - "end": 13 - }, - { - "label": "REGION", - "start": 16, - "end": 34 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 14, - "end": 15 - } - ] - }, - { - "source_row": 13, - "text": "Курганской области и Адыгее", - "gold": [ - { - "label": "REGION", - "start": 0, - "end": 18 - }, - { - "label": "REGION", - "start": 21, - "end": 27 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 19, - "end": 20 - }, - { - "label": "STREET", - "start": 0, - "end": 10 - } - ] - }, - { - "source_row": 23, - "text": "Преображенское район , Алатырь", - "gold": [ - { - "label": "DISTRICT", - "start": 0, - "end": 20 - }, - { - "label": "CITY", - "start": 23, - "end": 30 - } - ], - "predicted": [ - { - "label": "DISTRICT", - "start": 0, - "end": 14 - } - ] - }, - { - "source_row": 25, - "text": "Димитровград", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 12 - } - ], - "predicted": [] - }, - { - "source_row": 30, - "text": "Торжка", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 6 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 6 - } - ] - }, - { - "source_row": 37, - "text": "hIgh mEadow", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 11 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 0, - "end": 4 - }, - { - "label": "STREET", - "start": 5, - "end": 11 - } - ] - }, - { - "source_row": 37, - "text": "Hig Medow , ” “ HiGH MedDOw", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 9 - }, - { - "label": "CITY", - "start": 16, - "end": 27 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 0, - "end": 9 - } - ] - }, - { - "source_row": 37, - "text": "ВОЛОГОДСКАЯ", - "gold": [ - { - "label": "REGION", - "start": 0, - "end": 11 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 11 - } - ] - }, - { - "source_row": 37, - "text": "Mountain Rd", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 11 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 0, - "end": 8 - }, - { - "label": "STREET", - "start": 9, - "end": 11 - } - ] - }, - { - "source_row": 48, - "text": "ТУГУРО-ЧУМИКАНСКОМУ району Хабаровского края", - "gold": [ - { - "label": "DISTRICT", - "start": 0, - "end": 26 - }, - { - "label": "REGION", - "start": 27, - "end": 44 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 0, - "end": 6 - }, - { - "label": "STREET", - "start": 27, - "end": 44 - } - ] - }, - { - "source_row": 54, - "text": "ул . Крымский Вал , 10 , Бузулук", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 17 - }, - { - "label": "HOUSE", - "start": 20, - "end": 22 - }, - { - "label": "CITY", - "start": 25, - "end": 32 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 17 - } - ] - }, - { - "source_row": 91, - "text": "Тамбовская области", - "gold": [ - { - "label": "REGION", - "start": 0, - "end": 18 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 18 - } - ] - }, - { - "source_row": 96, - "text": "выборгском районе", - "gold": [ - { - "label": "DISTRICT", - "start": 0, - "end": 17 - } - ], - "predicted": [] - }, - { - "source_row": 101, - "text": "Лондоне", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 7 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 7 - } - ] - }, - { - "source_row": 143, - "text": "Новгородской области", - "gold": [ - { - "label": "REGION", - "start": 0, - "end": 20 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 20 - } - ] - }, - { - "source_row": 151, - "text": "пр . бауманская , д . 71", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 15 - }, - { - "label": "HOUSE", - "start": 18, - "end": 24 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 22, - "end": 24 - }, - { - "label": "REGION", - "start": 5, - "end": 15 - }, - { - "label": "STREET", - "start": 18, - "end": 19 - } - ] - }, - { - "source_row": 156, - "text": "Псковская области", - "gold": [ - { - "label": "REGION", - "start": 0, - "end": 17 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 10, - "end": 17 - }, - { - "label": "REGION", - "start": 0, - "end": 9 - } - ] - }, - { - "source_row": 156, - "text": "Магаданская области", - "gold": [ - { - "label": "REGION", - "start": 0, - "end": 19 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 19 - } - ] - }, - { - "source_row": 161, - "text": "Польша , district= Центральный округ", - "gold": [ - { - "label": "DISTRICT", - "start": 19, - "end": 36 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 0, - "end": 6 - }, - { - "label": "STREET", - "start": 9, - "end": 36 - } - ] - }, - { - "source_row": 163, - "text": "Подольск Курортном район", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 8 - }, - { - "label": "DISTRICT", - "start": 9, - "end": 24 - } - ], - "predicted": [ - { - "label": "DISTRICT", - "start": 0, - "end": 18 - } - ] - }, - { - "source_row": 164, - "text": "зЕлЕНоДолЬСК находится в Сахалинская крае", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 12 - }, - { - "label": "REGION", - "start": 25, - "end": 41 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 0, - "end": 12 - }, - { - "label": "REGION", - "start": 13, - "end": 22 - }, - { - "label": "STREET", - "start": 25, - "end": 41 - } - ] - }, - { - "source_row": 169, - "text": "люберцы", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 7 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 7 - } - ] - }, - { - "source_row": 185, - "text": "Новосибирску", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 12 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 12 - } - ] - }, - { - "source_row": 186, - "text": "Троицкий административный округ Новошахтинск", - "gold": [ - { - "label": "DISTRICT", - "start": 0, - "end": 31 - }, - { - "label": "CITY", - "start": 32, - "end": 44 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 32, - "end": 44 - }, - { - "label": "STREET", - "start": 0, - "end": 25 - } - ] - }, - { - "source_row": 186, - "text": "Бабушкинский , Бирюлёво , Вешняки", - "gold": [ - { - "label": "DISTRICT", - "start": 0, - "end": 12 - }, - { - "label": "DISTRICT", - "start": 15, - "end": 23 - }, - { - "label": "DISTRICT", - "start": 26, - "end": 33 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 15, - "end": 23 - }, - { - "label": "STREET", - "start": 0, - "end": 12 - } - ] - }, - { - "source_row": 193, - "text": "Калуге", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 6 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 6 - } - ] - }, - { - "source_row": 209, - "text": "замБИЯ адыгее , НЕВСКОМ район , Златоуст", - "gold": [ - { - "label": "REGION", - "start": 7, - "end": 13 - }, - { - "label": "DISTRICT", - "start": 16, - "end": 29 - }, - { - "label": "CITY", - "start": 32, - "end": 40 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 7, - "end": 13 - }, - { - "label": "DISTRICT", - "start": 16, - "end": 23 - }, - { - "label": "STREET", - "start": 0, - "end": 6 - } - ] - }, - { - "source_row": 218, - "text": "3-а , Каширское ш . , Вольск , Глазов , АМУРСКАЯ ОБЛ", - "gold": [ - { - "label": "HOUSE", - "start": 0, - "end": 3 - }, - { - "label": "STREET", - "start": 6, - "end": 17 - }, - { - "label": "CITY", - "start": 22, - "end": 28 - }, - { - "label": "CITY", - "start": 31, - "end": 37 - }, - { - "label": "REGION", - "start": 40, - "end": 52 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 22, - "end": 28 - }, - { - "label": "REGION", - "start": 40, - "end": 48 - }, - { - "label": "STREET", - "start": 6, - "end": 17 - } - ] - }, - { - "source_row": 220, - "text": "площади Восстания", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 17 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 0, - "end": 7 - }, - { - "label": "STREET", - "start": 8, - "end": 17 - } - ] - }, - { - "source_row": 226, - "text": "Чапаевск , Ямайка", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 8 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 0, - "end": 8 - }, - { - "label": "STREET", - "start": 11, - "end": 17 - } - ] - }, - { - "source_row": 230, - "text": "пр-т Ленина", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 11 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 3, - "end": 4 - }, - { - "label": "STREET", - "start": 5, - "end": 11 - } - ] - }, - { - "source_row": 230, - "text": "стр . 7А", - "gold": [ - { - "label": "HOUSE", - "start": 0, - "end": 8 - } - ], - "predicted": [ - { - "label": "HOUSE", - "start": 6, - "end": 8 - }, - { - "label": "STREET", - "start": 0, - "end": 3 - } - ] - }, - { - "source_row": 246, - "text": "АБХАЗИЯ , almatinskaya OBLAST G . TALDYKORGAN ul . abaya D . 10 KV . 4", - "gold": [ - { - "label": "REGION", - "start": 10, - "end": 29 - }, - { - "label": "CITY", - "start": 34, - "end": 45 - }, - { - "label": "STREET", - "start": 46, - "end": 56 - }, - { - "label": "HOUSE", - "start": 57, - "end": 70 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 0, - "end": 7 - }, - { - "label": "HOUSE", - "start": 69, - "end": 70 - }, - { - "label": "STREET", - "start": 64, - "end": 66 - } - ] - }, - { - "source_row": 259, - "text": "Чебоксарах", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 10 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 10 - } - ] - }, - { - "source_row": 262, - "text": "ROSSIYA , тАмБовсКАя OBLAST G . MYTISHCHI Стромынка D . 5 KV . 12", - "gold": [ - { - "label": "REGION", - "start": 10, - "end": 27 - }, - { - "label": "CITY", - "start": 32, - "end": 41 - }, - { - "label": "STREET", - "start": 42, - "end": 51 - }, - { - "label": "HOUSE", - "start": 52, - "end": 65 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 0, - "end": 7 - }, - { - "label": "HOUSE", - "start": 63, - "end": 65 - }, - { - "label": "REGION", - "start": 10, - "end": 20 - }, - { - "label": "STREET", - "start": 58, - "end": 60 - } - ] - }, - { - "source_row": 267, - "text": "Костромская области:", - "gold": [ - { - "label": "REGION", - "start": 0, - "end": 20 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 12, - "end": 19 - }, - { - "label": "REGION", - "start": 0, - "end": 11 - } - ] - }, - { - "source_row": 271, - "text": "Усолье-Сибирское", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 16 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 0, - "end": 6 - }, - { - "label": "STREET", - "start": 7, - "end": 16 - } - ] - }, - { - "source_row": 281, - "text": "Чукотский автономный округ", - "gold": [ - { - "label": "DISTRICT", - "start": 0, - "end": 26 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 26 - } - ] - }, - { - "source_row": 293, - "text": "Невинномысск г . УСОЛЬЕ-СИБИРСКОЕ , УЛ . ДУБИНИНСКАЯ , 70 стр . 1", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 12 - }, - { - "label": "CITY", - "start": 17, - "end": 33 - }, - { - "label": "STREET", - "start": 36, - "end": 52 - }, - { - "label": "HOUSE", - "start": 55, - "end": 65 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 17, - "end": 33 - }, - { - "label": "HOUSE", - "start": 64, - "end": 65 - }, - { - "label": "STREET", - "start": 36, - "end": 52 - } - ] - }, - { - "source_row": 298, - "text": "вологодская области", - "gold": [ - { - "label": "REGION", - "start": 0, - "end": 19 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 19 - } - ] - }, - { - "source_row": 307, - "text": "Ненецком крае", - "gold": [ - { - "label": "REGION", - "start": 0, - "end": 13 - } - ], - "predicted": [] - }, - { - "source_row": 312, - "text": "Сахалин", - "gold": [ - { - "label": "REGION", - "start": 0, - "end": 7 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 0, - "end": 7 - } - ] - }, - { - "source_row": 326, - "text": "Ардатова до Нижнего Ломова , от Чкаловска до Лукоянова , от Красных Прудов до границы Сергачского района", - "gold": [ - { - "label": "CITY", - "start": 0, - "end": 8 - }, - { - "label": "CITY", - "start": 12, - "end": 26 - }, - { - "label": "CITY", - "start": 32, - "end": 41 - }, - { - "label": "CITY", - "start": 45, - "end": 54 - }, - { - "label": "CITY", - "start": 60, - "end": 74 - }, - { - "label": "DISTRICT", - "start": 86, - "end": 104 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 12, - "end": 19 - }, - { - "label": "STREET", - "start": 0, - "end": 8 - } - ] - }, - { - "source_row": 330, - "text": "Латинский квартал", - "gold": [ - { - "label": "DISTRICT", - "start": 0, - "end": 17 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 17 - } - ] - }, - { - "source_row": 333, - "text": "Ульяновская области", - "gold": [ - { - "label": "REGION", - "start": 0, - "end": 19 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 12, - "end": 19 - }, - { - "label": "REGION", - "start": 0, - "end": 11 - } - ] - }, - { - "source_row": 339, - "text": "округу Барнстейбл", - "gold": [ - { - "label": "DISTRICT", - "start": 0, - "end": 17 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 17 - } - ] - }, - { - "source_row": 350, - "text": "пр . Хавская , д . 149 , кв . 230", - "gold": [ - { - "label": "STREET", - "start": 0, - "end": 12 - }, - { - "label": "HOUSE", - "start": 15, - "end": 33 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 0, - "end": 2 - }, - { - "label": "HOUSE", - "start": 30, - "end": 33 - }, - { - "label": "REGION", - "start": 5, - "end": 12 - }, - { - "label": "STREET", - "start": 25, - "end": 27 - } - ] - }, - { - "source_row": 357, - "text": "Адмиралтейском район в БРАТСК", - "gold": [ - { - "label": "DISTRICT", - "start": 0, - "end": 20 - }, - { - "label": "CITY", - "start": 23, - "end": 29 - } - ], - "predicted": [ - { - "label": "CITY", - "start": 21, - "end": 22 - }, - { - "label": "DISTRICT", - "start": 0, - "end": 14 - }, - { - "label": "STREET", - "start": 23, - "end": 29 - } - ] - }, - { - "source_row": 364, - "text": "Алтай", - "gold": [ - { - "label": "REGION", - "start": 0, - "end": 5 - } - ], - "predicted": [ - { - "label": "STREET", - "start": 0, - "end": 5 - } - ] - } - ], - "slices": { - "multi_field": { - "source_rows": 206, - "address_snippets": 217, - "matching": "one-to-one same-label span overlap; address windows are oracle-cropped from the benchmark's BIO annotations", - "micro": { - "tp": 419, - "fp": 130, - "fn": 170, - "support": 589, - "precision": 0.763206, - "recall": 0.711375, - "f1": 0.73638 - }, - "macro_field_f1": 0.7125, - "overlap_matches": 419, - "exact_span_matches": 212, - "exact_span_recall": 0.359932, - "fields": { - "REGION": { - "tp": 28, - "fp": 9, - "fn": 32, - "support": 60, - "precision": 0.756757, - "recall": 0.466667, - "f1": 0.57732 - }, - "DISTRICT": { - "tp": 36, - "fp": 2, - "fn": 30, - "support": 66, - "precision": 0.947368, - "recall": 0.545455, - "f1": 0.692308 - }, - "CITY": { - "tp": 104, - "fp": 61, - "fn": 51, - "support": 155, - "precision": 0.630303, - "recall": 0.670968, - "f1": 0.65 - }, - "STREET": { - "tp": 125, - "fp": 58, - "fn": 30, - "support": 155, - "precision": 0.68306, - "recall": 0.806452, - "f1": 0.739645 - }, - "HOUSE": { - "tp": 126, - "fp": 0, - "fn": 27, - "support": 153, - "precision": 1.0, - "recall": 0.823529, - "f1": 0.903226 - } - } - }, - "street_and_house": { - "source_rows": 135, - "address_snippets": 144, - "matching": "one-to-one same-label span overlap; address windows are oracle-cropped from the benchmark's BIO annotations", - "micro": { - "tp": 326, - "fp": 73, - "fn": 102, - "support": 428, - "precision": 0.817043, - "recall": 0.761682, - "f1": 0.788392 - }, - "macro_field_f1": 0.620307, - "overlap_matches": 326, - "exact_span_matches": 179, - "exact_span_recall": 0.418224, - "fields": { - "REGION": { - "tp": 9, - "fp": 7, - "fn": 9, - "support": 18, - "precision": 0.5625, - "recall": 0.5, - "f1": 0.529412 - }, - "DISTRICT": { - "tp": 1, - "fp": 2, - "fn": 7, - "support": 8, - "precision": 0.333333, - "recall": 0.125, - "f1": 0.181818 - }, - "CITY": { - "tp": 71, - "fp": 42, - "fn": 30, - "support": 101, - "precision": 0.628319, - "recall": 0.70297, - "f1": 0.663551 - }, - "STREET": { - "tp": 119, - "fp": 22, - "fn": 29, - "support": 148, - "precision": 0.843972, - "recall": 0.804054, - "f1": 0.823529 - }, - "HOUSE": { - "tp": 126, - "fp": 0, - "fn": 27, - "support": 153, - "precision": 1.0, - "recall": 0.823529, - "f1": 0.903226 - } - } - }, - "administrative_only": { - "source_rows": 334, - "address_snippets": 403, - "matching": "one-to-one same-label span overlap; address windows are oracle-cropped from the benchmark's BIO annotations", - "micro": { - "tp": 221, - "fp": 298, - "fn": 319, - "support": 540, - "precision": 0.425819, - "recall": 0.409259, - "f1": 0.417375 - }, - "macro_field_f1": 0.513519, - "overlap_matches": 221, - "exact_span_matches": 98, - "exact_span_recall": 0.181481, - "fields": { - "REGION": { - "tp": 56, - "fp": 3, - "fn": 101, - "support": 157, - "precision": 0.949153, - "recall": 0.356688, - "f1": 0.518519 - }, - "DISTRICT": { - "tp": 44, - "fp": 1, - "fn": 106, - "support": 150, - "precision": 0.977778, - "recall": 0.293333, - "f1": 0.451282 - }, - "CITY": { - "tp": 121, - "fp": 70, - "fn": 112, - "support": 233, - "precision": 0.633508, - "recall": 0.519313, - "f1": 0.570755 - }, - "STREET": { - "tp": 0, - "fp": 224, - "fn": 0, - "support": 0, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "HOUSE": { - "tp": 0, - "fp": 0, - "fn": 0, - "support": 0, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - } - } - } - } -} diff --git a/evaluation/release_gates.json b/evaluation/release_gates.json deleted file mode 100644 index 705e6a6..0000000 --- a/evaluation/release_gates.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "2.0.0a1 legacy-reference regression gate", - "scope": "Extraction-only rows from the legacy Good worksheet; not an independently reviewed nationwide benchmark.", - "minimum_rows": 500, - "minimum_metrics": { - "exact_address_rate": 0.8, - "no_unparsed_rate": 0.75, - "micro.f1": 0.955, - "fields.postal_code.f1": 0.999, - "fields.region.f1": 0.9, - "fields.city.f1": 0.985, - "fields.street.f1": 0.955, - "fields.street_type.f1": 0.9, - "fields.house_num.f1": 0.95, - "fields.corpus.f1": 0.95, - "fields.structure.f1": 0.96, - "fields.apartment.f1": 0.85 - } -} diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index c22a19b..0000000 --- a/examples/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# Examples - -All examples assume v2 is installed from the repository checkout: - -```bash -python -m pip install . -``` - -The core package remains offline and dependency-free. Examples that add a web -framework or contact a customer system say so explicitly. - -## Single address - -```bash -python examples/basic.py -python examples/basic.py "г. Москва, ул. Тверская, д.4, кв.12" -``` - -The program prints the full JSON-compatible result followed by its review -decision. - -## Address span in a message - -```bash -python examples/detect_in_message.py -``` - -The detector returns ordered half-open spans into the original message and a -`ParsedAddress` for each span. It deliberately requires strong address evidence -and does not treat every place name or number as an address. - -## Streaming ETL - -Each input line is treated as one raw address. Each output line is one JSON -object, so memory use does not grow with the file: - -```bash -printf '%s\n' \ - 'Ополченская 5-30' \ - 'Самара Авроры 7 12' | - python examples/jsonl_etl.py > parsed.jsonl -``` - -Blank lines are retained as empty parse results. Add a filter in the calling -pipeline if blank lines should instead be rejected. - -## FastAPI wrapper - -FastAPI and Uvicorn are optional application dependencies, not package runtime -dependencies: - -```bash -python -m pip install fastapi uvicorn -uvicorn examples.fastapi_app:app --reload -``` - -Then: - -```bash -curl -sS http://127.0.0.1:8000/parse \ - -H 'content-type: application/json' \ - -d '{"address":"СПб Невский проспект 10 корп 2 кв 15"}' -``` - -Add the authentication, request-size limits, rate limits, observability, and -deployment controls required by your environment before exposing this service. - -## Customer-managed FIAS/GAR resolver - -First inspect the proposed generic request without making a network call: - -```bash -python examples/fias_gar_http.py "Ополченская 5-30" -``` - -To send it to an endpoint you control: - -```bash -python examples/fias_gar_http.py \ - "Ополченская 5-30" \ - --resolver-url https://resolver.internal.example/v1/candidates \ - --send -``` - -The example defines an illustrative HTTP contract; FIAS/GAR does not impose -that contract. Adapt field names, authentication, candidate ranking, and -registry freshness policy to your system. `--send` is explicit because the -parser itself must never hide a network call. diff --git a/examples/basic.py b/examples/basic.py deleted file mode 100644 index 1653ed6..0000000 --- a/examples/basic.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Parse one address and make review routing explicit.""" - -from __future__ import annotations - -import argparse -import json - -from address_normalizer import ParsedAddress, parse - - -def needs_review(result: ParsedAddress) -> bool: - """Use structural evidence, not an uncalibrated global score threshold.""" - - required_fields_missing = result.street is None or result.house_num is None - unresolved_evidence = bool( - result.warnings or result.alternatives or result.unparsed - ) - return required_fields_missing or unresolved_evidence - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("address", nargs="?", default="Ополченская 5-30") - args = parser.parse_args() - - result = parse(args.address) - print(json.dumps(result.as_dict(), ensure_ascii=False, indent=2)) - print(f"needs_review={str(needs_review(result)).lower()}") - - -if __name__ == "__main__": - main() diff --git a/examples/detect_in_message.py b/examples/detect_in_message.py deleted file mode 100644 index 8d0105a..0000000 --- a/examples/detect_in_message.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Detect address spans in a free-form message without a registry lookup.""" - -from __future__ import annotations - -import json - -from address_normalizer import detect_addresses - - -MESSAGE = ( - "Курьер приедет по адресу: Москва, ул. Тверская, " - "д. 13, кв. 4. Позвоните заранее." -) - - -def main() -> None: - for detected in detect_addresses(MESSAGE): - assert MESSAGE[detected.start : detected.end] == detected.text - print(json.dumps(detected.as_dict(), ensure_ascii=False, indent=2)) - - -if __name__ == "__main__": - main() diff --git a/examples/fastapi_app.py b/examples/fastapi_app.py deleted file mode 100644 index a058370..0000000 --- a/examples/fastapi_app.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Optional FastAPI wrapper; FastAPI is not a package runtime dependency.""" - -from __future__ import annotations - -from address_normalizer import ParsedAddressDict, parse -from fastapi import FastAPI -from pydantic import BaseModel - - -class ParseRequest(BaseModel): - address: str - - -app = FastAPI(title="address-normalizer example", version="1") - - -@app.post("/parse") -def parse_address(request: ParseRequest) -> ParsedAddressDict: - """Extract fields; callers must still review or resolve the result.""" - - return parse(request.address).as_dict() diff --git a/examples/fias_gar_http.py b/examples/fias_gar_http.py deleted file mode 100644 index 3610fc7..0000000 --- a/examples/fias_gar_http.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Build or send a request to a customer-managed FIAS/GAR resolver. - -The package itself never performs this request. This illustrative contract must -be adapted to the resolver, authentication, and registry version you operate. -""" - -from __future__ import annotations - -import argparse -import json -from typing import Any -from urllib.request import Request, urlopen - -from address_normalizer import ParsedAddress, parse - - -FIELD_NAMES = ( - "postal_code", - "region", - "district", - "city", - "settlement", - "street", - "street_type", - "house_num", - "corpus", - "structure", - "apartment", -) - - -def resolver_payload(result: ParsedAddress) -> dict[str, Any]: - """Map selected values and ambiguity evidence to a generic resolver input.""" - - components = { - name: part.value - for name in FIELD_NAMES - if (part := getattr(result, name)) is not None - } - return { - "raw": result.raw, - "components": components, - "warnings": list(result.warnings), - "alternatives": [ - alternative.as_dict() for alternative in result.alternatives - ], - } - - -def send_json(url: str, payload: dict[str, Any]) -> Any: - """POST JSON using only the standard library.""" - - body = json.dumps(payload, ensure_ascii=False).encode("utf-8") - request = Request( - url, - data=body, - headers={"content-type": "application/json; charset=utf-8"}, - method="POST", - ) - with urlopen(request, timeout=10) as response: - return json.load(response) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("address") - parser.add_argument( - "--resolver-url", - default="http://127.0.0.1:8080/v1/candidates", - help="customer-managed endpoint; used only together with --send", - ) - parser.add_argument( - "--send", - action="store_true", - help="perform the network request; default is a local dry run", - ) - args = parser.parse_args() - - payload = resolver_payload(parse(args.address)) - print(json.dumps({"request": payload}, ensure_ascii=False, indent=2)) - - if args.send: - response = send_json(args.resolver_url, payload) - print(json.dumps({"response": response}, ensure_ascii=False, indent=2)) - - -if __name__ == "__main__": - main() diff --git a/examples/jsonl_etl.py b/examples/jsonl_etl.py deleted file mode 100644 index 0d0a61c..0000000 --- a/examples/jsonl_etl.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Convert newline-delimited raw addresses to newline-delimited parse results.""" - -from __future__ import annotations - -import json -import sys - -from address_normalizer import parse_iter - - -def main() -> None: - addresses = (line.rstrip("\r\n") for line in sys.stdin) - for source_line, result in enumerate(parse_iter(addresses), start=1): - record = { - "source_line": source_line, - "address": result.raw, - "parsed_address": result.as_dict(), - } - print(json.dumps(record, ensure_ascii=False)) - - -if __name__ == "__main__": - main() diff --git a/parsing.py b/parsing.py deleted file mode 100644 index ee46772..0000000 --- a/parsing.py +++ /dev/null @@ -1,283 +0,0 @@ -import re -import itertools -import operator - -import pandas as pd -from elasticsearch import Elasticsearch - -def inverdic(dic): - resdic = {} - for key, value in dic.items(): - for index in value: - if type(value) == set or type(value) == list: - if index in resdic.keys(): - if isinstance(resdic[index], set): - resdic[index].add(key) - elif isinstance(resdic[index], list): - resdic[index].append(key) - else: - resdic[index] = [resdic[index]] - resdic[index].append(key) - else: - resdic[index] = key - elif type(value) == dict: - resdic.update(inverdic(value)) - return resdic - - -""" -Словарь, который используется для детекции номера дома, корпуса и т.д. -""" -sep_house_signs = { - 'дом': {'д', 'дом'}, - 'владение': {'владение', 'вл'}, - 'корпус': {'к', 'корп', 'копр', 'кор', 'корпус'}, - 'строение': {'с', 'стр', 'строен', 'строение'}, - 'квартира': {'кв', 'квартира'}, - 'помещение': {'пом', 'помещение'}, - 'комната': {"ком", 'комн', 'комната'}, - 'кабинет': {"кабинет", "каб", "к-т", "каб-т"}, - 'офис': {'оф', 'офис'}, - 'литера': set("абвежз"), - 'прочее': {'литер', 'литера', 'лит'}, - 'дробь': {'/', '-'} -} - -house_signs_inv = inverdic(sep_house_signs) - - -def boost_keyword(dic): - replaces_lowered = {} - for key, value in replaces.items(): - if isinstance(key, tuple): - new_key = "(" + ' OR '.join(key) + ')^' + str(1 / len(key)) - replaces_lowered[new_key] = value - else: - replaces_lowered[key] = value - return replaces_lowered - - -''' -Этот словарь приводит все типы адресных объектов к стандартному виду (к тому что в ФИАС) -''' -replaces = { - 'обл': {"область", "обл", "обл-ть"}, - 'респ': {"республика", 'респ'}, - 'край': {'край'}, - 'г': {'г', 'гор', 'город'}, - ('ао', 'а.окр'): {'автономный округ', "автономный", 'аокр', 'а.окр'}, - ('а.обл', 'аобл'): {'автономная область', 'авт.обл', 'аобл', 'а обл', 'аобл'}, - - ('аллея', 'ал'): {'аллея', 'а', 'ал'}, - 'б-р': {'б-р', 'бульвар'}, - 'наб': {'наб', 'набережная'}, - 'пер': {'пер', 'переулок'}, - ('площадь', 'пл'): {'пл', "площадь"}, - ('проспект', 'пр-кт'): {"проспект", "пр", "пр-кт", "просп", 'пр-т'}, - "пр-д": {"проезд", "пр-д", "прд"}, - "ул": {"улица", "ул", "у", 'ул-ца'}, - - 'р-н': {'район', "р", "р-н"}, - 'п': {'поселок', 'посёлок', "пос"}, - 'пгт': {'поселок городского типа', 'посёлок городского типа', 'пос. гор. типа', 'пос.гор.типа', 'пос гор типа'}, - - 'г': {'г', 'гор', 'город'}, - 'с': {'с', 'село', 'сел'}, - 'д': {'д', 'дер', 'деревня', 'д-ня'}, - 'с/п': {"сельский поселок", "сельский посёлок", "сп", 'сельское поселение', 'сельпо', 'сп', 'сел.п.', } -} -replaces_inv = inverdic(boost_keyword(replaces)) - - -def del_sp_char(string): - ''' - Стоплист. Удаляет из строки символы переноса строки, но словарь можно дополнить при надобности - ''' - for stopword in {r'\n', r'\r', '\\', '(', ')', ':'}: - string = string.replace(stopword, ' ') - return re.sub(r"[\d]+", ' \g<0> ', string) - - -def preprocess(string): - ''' - Отделяет всё что можно друг от друга чтобы облегчить токенизацию - ''' - string = del_sp_char(string) - # Превращает "2c3" в "2 c 3" и "2-3" в "2 - 3" - string = re.sub(r"[\d]+|[\W]+", ' \g<0> ', string) - - string = string.replace(',', ', ') # то же самое, только с запятыми - string = re.sub(r'\,|\.|\-|\'|\"|\(|\)', '', string) - string = re.sub(r' +', ' ', string) - return string - - -def multiple_replace(dict, text, compiled=False): - ''' - Преобразует словарь замен (dict) в паттерн замен для регулярок и тут же применяет его - ''' - if not compiled: - regex = re.compile(r"\b(%s)\b" % "|".join(map(re.escape, dict.keys()))) - else: - regex = compiled - - # For each match, look-up corresponding value in dictionary - return regex.sub(lambda mo: dict[mo.string[mo.start():mo.end()]], text) - - -def tokenize(string, comma=False): - ''' - Токенизатор. Раздвигает слипшиеся буквы и цифры вроде 2с3 или корп1 - Вход: строка - Выход: массив из слов (токены) - ''' - # string = re.sub(r"[\d]+", ' \g<0> ', string).lower() — это уже сделано при препроцессинге - string = string.lower() - if not comma: - return re.findall(r'[\d]+|[\w]+', string) - if comma: - return re.findall(r'[\d]+|[\w]+|\,', string) - - -def tokens_to_string(tokens, string): - ''' - Ищет токены в строке и возвращает их позицию начала - ''' - pattern = r".?.?".join(tokens) - found = re.search(pattern, string.lower()) - if found == None: - split = len(string) - else: - split = found.start() - return split - - -def extract_index(string, errors=False): # 100% works !!! - ''' - Извлекает индекс из строки - Вход: строка - Выход: адрес без индекса, индекс - ''' - index = re.findall(r'[^| |,][\d]{5}[ |$|, ]', string) - if len(index) > 1 and errors: - print("Два индекса в строке \"%s\" ?" % string) - - if index != []: - index = index[0] - string = string.replace(index, '').strip() - index = index.replace(',', '') - else: - index = None - return string, index - - -def clarify_address(tokens, types): - ''' - Разбивает строку с номером дома на ещё более точные части. - Возвращает номер дома, корпуса и строения - ''' - # add missing "number" type - for i, token in enumerate(tokens): - if token.isdigit(): - types[i] = 'число' - if i == 0: - types[i] = "дом" - elif types[i - 1] in sep_house_signs and types[i - 1] != 'литера' and types[i - 1] != 'дробь': - types[i] = types[i - 1] - elif i >= 2 and types[i - 1] == 'дробь': - types[i] = types[i - 2] - types[i - 1] = types[i - 2] - elif types[i] == 'литера' and i != 0: - tokens[i - 1] += tokens[i] - - # write this info somewhere - dic = {} - for token, typ in zip(tokens, types): - if token not in house_signs_inv and typ != 'препинания': - if typ not in dic: - dic[typ] = token - - # rename - dic['Дом'] = dic.get('дом', '') - if len(dic.get('корпус', '')) > len(dic.get('строение', '')): - dic['Корпус/строение'] = dic['корпус'] - elif dic.get('строение', False): - dic['Корпус/строение'] = dic['строение'] - return dic - - -def extract_house_tokens(tokens): - ''' - находит последовательность номеров дома/корпуса/строения среди токенов и возвращает их - ''' - a = lambda x: "число" if x.isdigit() and len(x) < 6 else "препинания" if x == ',' else "не распознано" - types = [house_signs_inv.get(x, a(x)) for x in tokens] - types_bin = [0 if x == 'не распознано' else 1 for x in types] - array = list((list(y) for (x, y) in itertools.groupby((enumerate(types_bin)), operator.itemgetter(1)) if x == 1)) - if len(array) == 0: - return [], [] - longest_seq = max(reversed(array), key=len) - return [tokens[i] for (i, _) in longest_seq], [types[i] for (i, _) in longest_seq] - - -def extract_house(string): # from 2.0 - ''' - Обёртка для процедуры извлечения номера дома/корпуса от оставшейся строки - Вход: адрес(строка) - Выход: адрес без номеров дома/корпуса, номера дома/корпуса строкой - ''' - tokens = tokenize(string, comma=True) - house_tokens, house_types = extract_house_tokens(tokens) - - split = tokens_to_string(house_tokens, string) - - house = clarify_address(house_tokens, house_types) - address = string[:split] - # house = string[split:] - return address, house - - -stopwords = { - 'российская': '', - 'федерация': '', - 'орел': 'орёл', - 'мо': 'московская обл', - 'большой': "(б OR большой)", - 'большая': "(б OR большая)", - 'малый': "(м OR малый)", - 'малая': "(м OR малая)", - 'средний': '(ср OR с OR средний)', - 'средняя': '(ср OR с OR средняя)', - 'нижний': '(н OR нижний)', - 'б': "(б OR большая OR большой)", - 'с': '(ср OR с OR средняя OR средний)', - 'ср': '(ср OR с OR средняя OR средний)', - 'м': "(м OR малый OR малая)", - 'н': '(н OR нижний)', - '/': '' -} - - -def optimize_for_search(string): - ''' - вводит небольшие изменения в строку поиска для более точного поиска - ''' - string = string.replace('ё', 'е') - string = multiple_replace(stopwords, string.lower()) - string = multiple_replace(replaces_inv, string) - # string = re.sub(r"[а-яА-Я]{4,}", '\g<0>~^2', string) - return string - - -housenum_replaces = { - '/': '\/' -} - - -def optimize_housenum(string): - ''' - Пока что делает escape для "/" - ''' - # creates escape characters for elasticsearch - string = multiple_replace(housenum_replaces, string) - return '"' + string + '"' diff --git a/pyproject.toml b/pyproject.toml index 91024dd..e935795 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,4 +64,4 @@ warn_unreachable = true [tool.pytest.ini_options] addopts = "-q" -testpaths = ["tests_v2"] +testpaths = ["tests"] diff --git a/ref/references.xlsx b/ref/references.xlsx deleted file mode 100644 index 07e5e2c663443f1991a29dd741a4f2b603e9dffe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 600927 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 QB`VtPX^V>-qy?e~2p@ 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 z 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 z

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�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੝QM%Zfk+ht$(u-@( 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 mhv-8P?NC@MbmAvI+a}3jfwC3A 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

`>_&~#R_i>%)XS7WR7YvV}<4{xSJdJDM_|Q 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 diff --git a/release-policy.toml b/release-policy.toml deleted file mode 100644 index fc4b195..0000000 --- a/release-policy.toml +++ /dev/null @@ -1,16 +0,0 @@ -schema_version = 1 - -[publication] -# Maintainer decisions and their repository-local evidence. Publication requires -# both statuses to remain "approved". -license_status = "approved" -license_evidence = ["LICENSE", "LICENSING.md"] -model_provenance_status = "approved" -model_provenance_evidence = [ - "LICENSING.md", - "ref/references.xlsx", - "evaluation/legacy_reference_500.jsonl", - "training/README.md", - "training/model_evaluation.json", - "src/address_normalizer/data/model.json", -] diff --git a/requirements-evaluation.txt b/requirements-evaluation.txt deleted file mode 100644 index b2536a2..0000000 --- a/requirements-evaluation.txt +++ /dev/null @@ -1,4 +0,0 @@ -# Runtime installation remains dependency-free. This extra is only needed to -# prepare the large external Parquet benchmark. -pyarrow==25.0.0 -pymongo==4.17.0 diff --git a/requirements-legacy.txt b/requirements-legacy.txt deleted file mode 100644 index 6b58039..0000000 --- a/requirements-legacy.txt +++ /dev/null @@ -1,4 +0,0 @@ -elasticsearch -pandas -simpledbf -tqdm diff --git a/scripts/build_reproducibly.py b/scripts/build_reproducibly.py deleted file mode 100644 index dc8dfc4..0000000 --- a/scripts/build_reproducibly.py +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env python3 -"""Build twice with a fixed epoch and keep only byte-identical artifacts.""" - -from __future__ import annotations - -import argparse -import copy -import gzip -from hashlib import sha256 -import os -from pathlib import Path -import subprocess -import sys -import tarfile -import tempfile - - -DEFAULT_SOURCE_DATE_EPOCH = "1704067200" # 2024-01-01T00:00:00Z - - -def canonicalize_sdist(path: Path, epoch: int) -> None: - """Remove ambient filesystem and gzip metadata from a setuptools sdist.""" - temporary = path.with_name(f".{path.name}.canonical") - try: - with tarfile.open(path, mode="r:gz") as source: - members = source.getmembers() - with temporary.open("wb") as raw_output: - with gzip.GzipFile( - filename="", - mode="wb", - fileobj=raw_output, - compresslevel=9, - mtime=epoch, - ) as compressed: - with tarfile.open( - fileobj=compressed, - mode="w", - format=tarfile.PAX_FORMAT, - ) as target: - for original in sorted(members, key=lambda member: member.name): - member = copy.copy(original) - member.uid = 0 - member.gid = 0 - member.uname = "" - member.gname = "" - member.mtime = epoch - member.mode = 0o755 if member.isdir() else 0o644 - member.pax_headers = {} - file_data = source.extractfile(original) if original.isfile() else None - target.addfile(member, file_data) - os.replace(temporary, path) - finally: - temporary.unlink(missing_ok=True) - - -def artifact_bytes(directory: Path) -> dict[str, bytes]: - artifacts = { - path.name: path.read_bytes() - for pattern in ("*.whl", "*.tar.gz") - for path in directory.glob(pattern) - } - if len([name for name in artifacts if name.endswith(".whl")]) != 1: - raise SystemExit("reproducible build failed: expected exactly one wheel") - if len([name for name in artifacts if name.endswith(".tar.gz")]) != 1: - raise SystemExit("reproducible build failed: expected exactly one sdist") - return artifacts - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--output", type=Path, default=Path("dist")) - parser.add_argument( - "--source-date-epoch", - default=os.environ.get("SOURCE_DATE_EPOCH", DEFAULT_SOURCE_DATE_EPOCH), - ) - args = parser.parse_args() - if not args.source_date_epoch.isdigit(): - raise SystemExit("reproducible build failed: SOURCE_DATE_EPOCH must be an integer") - if args.output.exists() and any(args.output.iterdir()): - raise SystemExit(f"reproducible build failed: output is not empty: {args.output}") - - environment = os.environ.copy() - environment["SOURCE_DATE_EPOCH"] = args.source_date_epoch - with tempfile.TemporaryDirectory(prefix="address-normalizer-build-a-") as first_dir: - with tempfile.TemporaryDirectory(prefix="address-normalizer-build-b-") as second_dir: - first = Path(first_dir) - second = Path(second_dir) - for output in (first, second): - subprocess.run( - [sys.executable, "-m", "build", "--outdir", str(output)], - check=True, - env=environment, - ) - sdist = next(output.glob("*.tar.gz")) - canonicalize_sdist(sdist, int(args.source_date_epoch)) - first_artifacts = artifact_bytes(first) - second_artifacts = artifact_bytes(second) - if first_artifacts != second_artifacts: - details = [] - for name in sorted(first_artifacts.keys() | second_artifacts.keys()): - first_hash = ( - sha256(first_artifacts[name]).hexdigest() - if name in first_artifacts - else "missing" - ) - second_hash = ( - sha256(second_artifacts[name]).hexdigest() - if name in second_artifacts - else "missing" - ) - details.append(f"{name}: first={first_hash}, second={second_hash}") - raise SystemExit( - "reproducible build failed: artifacts are not byte-identical; " - + "; ".join(details) - ) - - args.output.mkdir(parents=True, exist_ok=True) - for name, content in first_artifacts.items(): - target = args.output / name - target.write_bytes(content) - print(f"{name}: {len(content)} bytes sha256={sha256(content).hexdigest()}") - - print(f"reproducible build: OK (SOURCE_DATE_EPOCH={args.source_date_epoch})") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check_artifacts.py b/scripts/check_artifacts.py deleted file mode 100644 index 375a803..0000000 --- a/scripts/check_artifacts.py +++ /dev/null @@ -1,434 +0,0 @@ -#!/usr/bin/env python3 -"""Inspect built distributions and write a small provenance manifest.""" - -from __future__ import annotations - -import argparse -import base64 -import csv -from email.parser import BytesParser -from hashlib import sha256 -import io -import json -import os -from pathlib import Path, PurePosixPath -import platform -import re -import subprocess -import tarfile -import tomllib -import zipfile - - -ROOT = Path(__file__).resolve().parents[1] -PACKAGE_ROOT = ROOT / "src" / "address_normalizer" -MODEL_PATH = PACKAGE_ROOT / "data" / "model.json" -WHEEL_MAX_BYTES = 256 * 1024 -SDIST_MAX_BYTES = 256 * 1024 -MODEL_MAX_BYTES = 64 * 1024 - -FORBIDDEN_PARTS = { - ".cache", - ".git", - ".github", - ".ipynb_checkpoints", - "__pycache__", - "evaluation", - "ref", - "tests", - "tests_v2", - "training", -} -FORBIDDEN_SUFFIXES = { - ".ipynb", - ".jsonl", - ".pem", - ".pyc", - ".pyo", - ".sqlite", - ".xlsx", -} - - -def fail(message: str) -> None: - raise SystemExit(f"artifact check failed: {message}") - - -def package_version() -> str: - init_text = (PACKAGE_ROOT / "__init__.py").read_text(encoding="utf-8") - match = re.search( - r'^__version__\s*=\s*["\'](?P[^"\']+)["\']\s*$', - init_text, - flags=re.MULTILINE, - ) - if match is None: - fail("address_normalizer.__version__ must be a string literal") - return match.group("version") - - -def expected_package_files() -> set[str]: - expected: set[str] = set() - for path in PACKAGE_ROOT.rglob("*"): - if not path.is_file() or "__pycache__" in path.parts: - continue - relative = path.relative_to(PACKAGE_ROOT).as_posix() - if path.suffix == ".py" or relative in {"py.typed", "data/model.json"}: - expected.add(f"address_normalizer/{relative}") - continue - fail(f"unexpected source-package file {relative}") - return expected - - -def project_metadata() -> dict[str, object]: - return tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))[ - "project" - ] - - -def configured_license_files() -> set[str]: - patterns = project_metadata().get("license-files", []) - if not isinstance(patterns, list): - fail("project.license-files must be a list when present") - return { - path.relative_to(ROOT).as_posix() - for pattern in patterns - if isinstance(pattern, str) - for path in ROOT.glob(pattern) - if path.is_file() - } - - -def expected_sdist_documentation() -> set[str]: - expected = { - "CHANGELOG.md", - "CONTRIBUTING.md", - "LICENSING.md", - "MANIFEST.in", - "README.md", - "README.ru.md", - "SECURITY.md", - "SUPPORT.md", - "pyproject.toml", - } - allowed_suffixes = {"docs": {".md"}, "examples": {".md", ".py"}} - for directory, suffixes in allowed_suffixes.items(): - for path in (ROOT / directory).rglob("*"): - if ( - not path.is_file() - or "__pycache__" in path.parts - or path.suffix in {".pyc", ".pyo"} - ): - continue - if path.suffix not in suffixes: - fail(f"unexpected {directory} artifact {path.relative_to(ROOT)}") - expected.add(path.relative_to(ROOT).as_posix()) - return expected - - -def safe_archive_name(name: str) -> PurePosixPath: - if "\\" in name: - fail(f"archive member uses a backslash: {name!r}") - path = PurePosixPath(name) - if path.is_absolute() or ".." in path.parts: - fail(f"unsafe archive member: {name!r}") - return path - - -def forbidden_member(name: str) -> bool: - path = safe_archive_name(name) - lower_parts = {part.lower() for part in path.parts} - return bool(lower_parts & FORBIDDEN_PARTS) or path.suffix.lower() in FORBIDDEN_SUFFIXES - - -def verify_record(archive: zipfile.ZipFile, record_name: str) -> None: - rows = list(csv.reader(io.StringIO(archive.read(record_name).decode("utf-8")))) - recorded = {row[0]: row[1:] for row in rows} - if set(recorded) != set(archive.namelist()): - fail("wheel RECORD does not enumerate every archive member exactly once") - - for name in archive.namelist(): - digest, size = recorded[name] - if name == record_name: - if digest or size: - fail("wheel RECORD must not hash itself") - continue - if not digest.startswith("sha256="): - fail(f"wheel RECORD lacks a SHA-256 digest for {name}") - expected_digest = digest.removeprefix("sha256=") - actual_digest = base64.urlsafe_b64encode( - sha256(archive.read(name)).digest() - ).rstrip(b"=").decode("ascii") - if expected_digest != actual_digest: - fail(f"wheel RECORD digest mismatch for {name}") - if size != str(len(archive.read(name))): - fail(f"wheel RECORD size mismatch for {name}") - - -def check_wheel(path: Path, expected_version: str) -> dict[str, object]: - if path.stat().st_size > WHEEL_MAX_BYTES: - fail(f"wheel is {path.stat().st_size} bytes; limit is {WHEEL_MAX_BYTES}") - - with zipfile.ZipFile(path) as archive: - names = archive.namelist() - if len(names) != len(set(names)): - fail("wheel contains duplicate member names") - for name in names: - safe_archive_name(name) - if forbidden_member(name): - fail(f"wheel contains forbidden member {name}") - - expected_files = expected_package_files() - package_files = {name for name in names if name.startswith("address_normalizer/")} - if package_files != expected_files: - missing = sorted(expected_files - package_files) - unexpected = sorted(package_files - expected_files) - fail(f"wheel package files differ; missing={missing}, unexpected={unexpected}") - - metadata_names = [name for name in names if name.endswith(".dist-info/METADATA")] - record_names = [name for name in names if name.endswith(".dist-info/RECORD")] - wheel_names = [name for name in names if name.endswith(".dist-info/WHEEL")] - if len(metadata_names) != 1 or len(record_names) != 1 or len(wheel_names) != 1: - fail("wheel must contain one METADATA, WHEEL, and RECORD file") - - metadata = BytesParser().parsebytes(archive.read(metadata_names[0])) - if metadata["Name"] != "address-normalizer": - fail(f"unexpected distribution name {metadata['Name']!r}") - if metadata["Version"] != expected_version: - fail( - f"metadata version {metadata['Version']!r} does not match " - f"source version {expected_version!r}" - ) - if metadata.get_all("Requires-Dist"): - fail(f"runtime dependencies found: {metadata.get_all('Requires-Dist')}") - if metadata["Requires-Python"] != ">=3.10": - fail(f"unexpected Requires-Python value {metadata['Requires-Python']!r}") - dist_info = metadata_names[0].rsplit("/", 1)[0] - expected_dist_info = f"address_normalizer-{expected_version}.dist-info" - if dist_info != expected_dist_info: - fail(f"unexpected dist-info directory {dist_info!r}") - - project = project_metadata() - expected_license = project.get("license") - actual_license = metadata["License-Expression"] - if expected_license != actual_license: - fail( - f"wheel license expression {actual_license!r} does not match " - f"pyproject value {expected_license!r}" - ) - license_files = configured_license_files() - metadata_license_files = set(metadata.get_all("License-File", [])) - if metadata_license_files != license_files: - fail( - f"wheel License-File metadata differs; expected={sorted(license_files)}, " - f"actual={sorted(metadata_license_files)}" - ) - - allowed_dist_info = { - f"{dist_info}/METADATA", - f"{dist_info}/RECORD", - f"{dist_info}/WHEEL", - f"{dist_info}/entry_points.txt", - f"{dist_info}/top_level.txt", - *(f"{dist_info}/licenses/{name}" for name in license_files), - } - allowed_names = expected_files | allowed_dist_info - if set(names) != allowed_names: - missing = sorted(allowed_names - set(names)) - unexpected = sorted(set(names) - allowed_names) - fail(f"wheel members differ; missing={missing}, unexpected={unexpected}") - - wheel_metadata = archive.read(wheel_names[0]).decode("utf-8") - if "Tag: py3-none-any" not in wheel_metadata: - fail("wheel is not tagged as platform-independent py3-none-any") - - model_bytes = archive.read("address_normalizer/data/model.json") - if model_bytes != MODEL_PATH.read_bytes(): - fail("wheel model differs from the source model") - if len(model_bytes) > MODEL_MAX_BYTES: - fail(f"model is {len(model_bytes)} bytes; limit is {MODEL_MAX_BYTES}") - try: - json.loads(model_bytes) - except (UnicodeDecodeError, json.JSONDecodeError) as error: - fail(f"bundled model is not valid UTF-8 JSON: {error}") - - verify_record(archive, record_names[0]) - - return { - "file": path.name, - "sha256": sha256(path.read_bytes()).hexdigest(), - "size": path.stat().st_size, - } - - -def check_sdist(path: Path, expected_version: str) -> dict[str, object]: - if path.stat().st_size > SDIST_MAX_BYTES: - fail(f"sdist is {path.stat().st_size} bytes; limit is {SDIST_MAX_BYTES}") - - expected_root = f"address_normalizer-{expected_version}" - with tarfile.open(path, mode="r:gz") as archive: - members = archive.getmembers() - names = [member.name for member in members] - if len(names) != len(set(names)): - fail("sdist contains duplicate member names") - for member in members: - member_path = safe_archive_name(member.name) - if not member_path.parts or member_path.parts[0] != expected_root: - fail(f"sdist member is outside {expected_root}: {member.name}") - if member.issym() or member.islnk() or member.isdev(): - fail(f"sdist contains a link or device: {member.name}") - if forbidden_member("/".join(member_path.parts[1:])): - fail(f"sdist contains forbidden member {member.name}") - - relative_files = { - "/".join(safe_archive_name(member.name).parts[1:]) - for member in members - if member.isfile() - } - expected_files = { - *expected_sdist_documentation(), - "PKG-INFO", - *(f"src/{name}" for name in expected_package_files()), - "setup.cfg", - "src/address_normalizer.egg-info/PKG-INFO", - "src/address_normalizer.egg-info/SOURCES.txt", - "src/address_normalizer.egg-info/dependency_links.txt", - "src/address_normalizer.egg-info/entry_points.txt", - "src/address_normalizer.egg-info/top_level.txt", - *configured_license_files(), - } - if relative_files != expected_files: - missing = sorted(expected_files - relative_files) - unexpected = sorted(relative_files - expected_files) - fail(f"sdist files differ; missing={missing}, unexpected={unexpected}") - - expected_directories = {""} - for name in expected_files: - parent = PurePosixPath(name).parent - while str(parent) != ".": - expected_directories.add(parent.as_posix()) - parent = parent.parent - relative_directories = { - "/".join(safe_archive_name(member.name).parts[1:]) - for member in members - if member.isdir() - } - if relative_directories != expected_directories: - missing = sorted(expected_directories - relative_directories) - unexpected = sorted(relative_directories - expected_directories) - fail(f"sdist directories differ; missing={missing}, unexpected={unexpected}") - - model_member = archive.extractfile( - f"{expected_root}/src/address_normalizer/data/model.json" - ) - if model_member is None or model_member.read() != MODEL_PATH.read_bytes(): - fail("sdist model differs from the source model") - - return { - "file": path.name, - "sha256": sha256(path.read_bytes()).hexdigest(), - "size": path.stat().st_size, - } - - -def git_details() -> tuple[str | None, bool | None]: - try: - revision = subprocess.run( - ["git", "rev-parse", "HEAD"], - cwd=ROOT, - check=True, - capture_output=True, - text=True, - ).stdout.strip() - status = subprocess.run( - ["git", "status", "--porcelain"], - cwd=ROOT, - check=True, - capture_output=True, - text=True, - ).stdout - except (FileNotFoundError, subprocess.CalledProcessError): - return None, None - return revision, bool(status) - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("dist", type=Path, help="directory containing one wheel and one sdist") - parser.add_argument( - "--expected-version", - help="fail unless built metadata matches this version as well as the source", - ) - parser.add_argument("--write-manifest", type=Path) - parser.add_argument( - "--verify-manifest", - type=Path, - help="verify artifacts and source provenance exactly match a prior manifest", - ) - args = parser.parse_args() - - wheels = sorted(args.dist.glob("*.whl")) - sdists = sorted(args.dist.glob("*.tar.gz")) - if len(wheels) != 1 or len(sdists) != 1: - fail( - f"expected exactly one wheel and one .tar.gz in {args.dist}; " - f"found {len(wheels)} wheel(s) and {len(sdists)} sdist(s)" - ) - - version = package_version() - if args.expected_version is not None and args.expected_version != version: - fail( - f"requested version {args.expected_version!r} does not match " - f"source version {version!r}" - ) - - pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) - if pyproject["project"].get("dependencies") != []: - fail("pyproject runtime dependencies must remain an explicit empty list") - if pyproject["build-system"]["requires"] != ["setuptools==80.9.0"]: - fail("build backend must stay exactly pinned for reproducible builds") - - artifacts = [ - check_wheel(wheels[0], version), - check_sdist(sdists[0], version), - ] - revision, dirty = git_details() - manifest = { - "schema_version": 1, - "distribution": "address-normalizer", - "version": version, - "source_revision": os.environ.get("GITHUB_SHA", revision), - "source_tree_dirty": dirty, - "build_python": platform.python_version(), - "build_backend": "setuptools==80.9.0", - "runtime_dependencies": [], - "model": { - "file": "src/address_normalizer/data/model.json", - "sha256": sha256(MODEL_PATH.read_bytes()).hexdigest(), - "size": MODEL_PATH.stat().st_size, - }, - "artifacts": artifacts, - } - if args.verify_manifest: - expected_manifest = json.loads(args.verify_manifest.read_text(encoding="utf-8")) - if expected_manifest != manifest: - fail( - f"current artifact provenance does not match {args.verify_manifest}; " - f"expected={json.dumps(expected_manifest, sort_keys=True)}, " - f"actual={json.dumps(manifest, sort_keys=True)}" - ) - if args.write_manifest: - args.write_manifest.parent.mkdir(parents=True, exist_ok=True) - args.write_manifest.write_text( - json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", - encoding="utf-8", - ) - - for artifact in artifacts: - print(f"{artifact['file']}: {artifact['size']} bytes sha256={artifact['sha256']}") - print(f"model.json: {MODEL_PATH.stat().st_size} bytes") - print("artifact policy: OK") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check_license.py b/scripts/check_license.py deleted file mode 100644 index d0a7e20..0000000 --- a/scripts/check_license.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -"""Enforce the recorded license and model-provenance release state.""" - -from __future__ import annotations - -import argparse -from pathlib import Path -import tomllib - - -ROOT = Path(__file__).resolve().parents[1] -LICENSE_NAMES = ("LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING", "COPYING.txt") -BLOCKER_TEXT = "No license currently applies to this repository" -POLICY_PATH = ROOT / "release-policy.toml" - - -def fail(message: str) -> None: - raise SystemExit(f"license gate failed: {message}") - - -def main() -> int: - parser = argparse.ArgumentParser() - mode = parser.add_mutually_exclusive_group(required=True) - mode.add_argument("--expect-blocked", action="store_true") - mode.add_argument("--require-publishable", action="store_true") - args = parser.parse_args() - - project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))[ - "project" - ] - policy = tomllib.loads(POLICY_PATH.read_text(encoding="utf-8")) - if policy.get("schema_version") != 1: - fail("release-policy.toml has an unsupported schema") - publication = policy.get("publication") - if not isinstance(publication, dict): - fail("release-policy.toml lacks a [publication] table") - license_status = publication.get("license_status") - model_status = publication.get("model_provenance_status") - license_expression = project.get("license") - license_patterns = project.get("license-files") - license_files = [ROOT / name for name in LICENSE_NAMES if (ROOT / name).is_file()] - blocker = ROOT / "LICENSING.md" - blocker_is_current = blocker.is_file() and BLOCKER_TEXT in blocker.read_text( - encoding="utf-8" - ) - - if args.expect_blocked: - if license_status != "blocked" or model_status != "blocked": - fail("both release-policy.toml publication statuses must remain blocked") - if license_expression or license_patterns or license_files: - fail("license metadata or a license file appeared; update the release policy deliberately") - if not blocker_is_current: - fail("LICENSING.md no longer records the known publication blocker") - print( - "publication status: BLOCKED by license and compact-model provenance; " - "package publication must remain disabled" - ) - return 0 - - if license_status != "approved": - fail("release-policy.toml license_status is not approved") - if model_status != "approved": - fail("release-policy.toml model_provenance_status is not approved") - for key in ("license_evidence", "model_provenance_evidence"): - evidence = publication.get(key) - if not isinstance(evidence, list) or not evidence: - fail(f"release-policy.toml {key} must list recorded evidence") - for item in evidence: - if not isinstance(item, str) or not (ROOT / item).is_file(): - fail(f"release-policy.toml {key} references a missing file: {item!r}") - if not isinstance(license_expression, str) or not license_expression.strip(): - fail("project.license must contain the maintainer-approved SPDX expression") - if not isinstance(license_patterns, list) or not license_patterns: - fail("project.license-files must identify the approved license file") - matched_files = [ - path - for pattern in license_patterns - for path in ROOT.glob(pattern) - if path.is_file() - ] - if not matched_files: - fail("project.license-files does not match a repository file") - if blocker_is_current: - fail("LICENSING.md still says that no license applies") - print(f"license status: publishable ({license_expression})") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check_release_ref.py b/scripts/check_release_ref.py deleted file mode 100644 index 9b3a76e..0000000 --- a/scripts/check_release_ref.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -"""Require a release workflow to run from the immutable matching version tag.""" - -from __future__ import annotations - -import argparse -import os -from pathlib import Path -import re -import subprocess - - -ROOT = Path(__file__).resolve().parents[1] -RELEASE_VERSION = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:(?:a|b|rc)[0-9]+)?$") - - -def fail(message: str) -> None: - raise SystemExit(f"release ref check failed: {message}") - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--version", required=True) - args = parser.parse_args() - - if RELEASE_VERSION.fullmatch(args.version) is None: - fail(f"{args.version!r} is not an allowed alpha/beta/rc/stable version") - expected_tag = f"v{args.version}" - ref_type = os.environ.get("GITHUB_REF_TYPE") - ref_name = os.environ.get("GITHUB_REF_NAME") - revision = os.environ.get("GITHUB_SHA") - if ref_type != "tag" or ref_name != expected_tag: - fail( - f"workflow must be dispatched from tag {expected_tag!r}; " - f"received ref_type={ref_type!r}, ref_name={ref_name!r}" - ) - if not revision: - fail("GITHUB_SHA is missing") - - tagged_commit = subprocess.run( - ["git", "rev-parse", f"refs/tags/{expected_tag}^{{commit}}"], - cwd=ROOT, - check=True, - capture_output=True, - text=True, - ).stdout.strip() - checked_out_commit = subprocess.run( - ["git", "rev-parse", "HEAD^{commit}"], - cwd=ROOT, - check=True, - capture_output=True, - text=True, - ).stdout.strip() - if tagged_commit != revision or checked_out_commit != revision: - fail( - f"tag, GITHUB_SHA, and checkout differ: " - f"tag={tagged_commit}, github={revision}, checkout={checked_out_commit}" - ) - print(f"release ref: OK ({expected_tag} -> {revision})") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/smoke_installed.py b/scripts/smoke_installed.py deleted file mode 100644 index 9870def..0000000 --- a/scripts/smoke_installed.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -"""Smoke-test an installed wheel without source-tree or network access.""" - -from __future__ import annotations - -from contextlib import redirect_stdout -import importlib.metadata -import io -import json -from pathlib import Path -import sys - - -def deny_network(event: str, _args: tuple[object, ...]) -> None: - if event.startswith(("socket.", "urllib.", "http.client.")): - raise RuntimeError(f"unexpected network operation: {event}") - - -def main() -> int: - sys.addaudithook(deny_network) - - import address_normalizer - from address_normalizer import parse, parse_many - from address_normalizer.cli import main as cli_main - - package_path = Path(address_normalizer.__file__).resolve() - if "site-packages" not in package_path.parts: - raise AssertionError(f"not importing an installed wheel: {package_path}") - if importlib.metadata.requires("address-normalizer"): - raise AssertionError("the installed distribution has runtime dependencies") - if importlib.metadata.version("address-normalizer") != address_normalizer.__version__: - raise AssertionError("runtime and distribution versions differ") - - result = parse("г. Москва, ул. Тверская, д.4, кв.12") - assert result.house_num is not None and result.house_num.value == "4" - assert len(parse_many(["Ополченская 5-30", "Невский проспект 10"])) == 2 - - single_output = io.StringIO() - with redirect_stdout(single_output): - assert cli_main(["Москва", "Тверская", "1"]) == 0 - json.loads(single_output.getvalue()) - - old_stdin = sys.stdin - batch_output = io.StringIO() - try: - sys.stdin = io.StringIO("Ополченская 5-30\nНевский проспект 10\n") - with redirect_stdout(batch_output): - assert cli_main(["--jsonl"]) == 0 - finally: - sys.stdin = old_stdin - lines = batch_output.getvalue().splitlines() - assert len(lines) == 2 - for line in lines: - json.loads(line) - - print(f"installed smoke: OK ({package_path})") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/test_artifact_policy.py b/scripts/test_artifact_policy.py deleted file mode 100644 index b1c7cfa..0000000 --- a/scripts/test_artifact_policy.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -"""Focused negative tests for distribution allowlists.""" - -from __future__ import annotations - -import argparse -from pathlib import Path -import shutil -import tarfile -import tempfile -import zipfile - -import check_artifacts - - -def expect_failure(action: object, expected_text: str) -> None: - try: - action() # type: ignore[operator] - except SystemExit as error: - if expected_text not in str(error): - raise AssertionError(f"unexpected checker failure: {error}") from error - else: - raise AssertionError("malicious archive unexpectedly passed") - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("dist", type=Path) - args = parser.parse_args() - wheel = next(args.dist.glob("*.whl")) - sdist = next(args.dist.glob("*.tar.gz")) - version = check_artifacts.package_version() - - with tempfile.TemporaryDirectory(prefix="artifact-policy-test-") as temp_dir: - temp = Path(temp_dir) - bad_wheel = temp / wheel.name - shutil.copyfile(wheel, bad_wheel) - with zipfile.ZipFile(bad_wheel, mode="a") as archive: - archive.writestr("payload.sh", "#!/bin/sh\n") - expect_failure( - lambda: check_artifacts.check_wheel(bad_wheel, version), - "wheel members differ", - ) - - bad_sdist = temp / sdist.name - expected_root = f"address_normalizer-{version}" - with tarfile.open(sdist, mode="r:gz") as source: - with tarfile.open(bad_sdist, mode="w:gz") as target: - for member in source.getmembers(): - target.addfile(member, source.extractfile(member) if member.isfile() else None) - payload = b"raise RuntimeError('unexpected source payload')\n" - member = tarfile.TarInfo(f"{expected_root}/src/evil.py") - member.size = len(payload) - import io - - target.addfile(member, io.BytesIO(payload)) - expect_failure( - lambda: check_artifacts.check_sdist(bad_sdist, version), - "sdist files differ", - ) - - print("artifact policy negative tests: OK") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/address_normalizer/data/model.json b/src/address_normalizer/data/model.json index beb575e..d5e4592 100644 --- a/src/address_normalizer/data/model.json +++ b/src/address_normalizer/data/model.json @@ -1 +1 @@ -{"format":"address-normalizer-compact-sequence-v1","training":{"algorithm":"epoch-averaged structured perceptron","seed":2017,"epochs":10,"examples":359,"source":"source-verifiable legacy reference rows with deterministic marker-free views","dataset":"evaluation/legacy_reference_500.jsonl","dataset_sha256":"853916e36cfc5a52d06add0524ba64ca2c771073c1c4e98cd5645116e7f74588","split":"SHA-256 by canonical address group: 70/15/15"},"labels":["O","REGION","DISTRICT","CITY","SETTLEMENT","STREET"],"emissions":{"CITY\tbias":0.7,"CITY\tbos":1.1,"CITY\teos":0.9,"CITY\tkind=word":0.7,"CITY\tnext=б":-1.8,"CITY\tnext=большой":1.0,"CITY\tnext=буденного":1.0,"CITY\tnext=вал":-0.9,"CITY\tnext=видное":-1.0,"CITY\tnext=вокзальная":-1.0,"CITY\tnext=володарского":0.9,"CITY\tnext=воронина":1.0,"CITY\tnext=г":-3.0,"CITY\tnext=гагарина":1.9,"CITY\tnext=гоголя":1.0,"CITY\tnext=донского":-1.0,"CITY\tnext=й":-1.0,"CITY\tnext=калинина":-1.0,"CITY\tnext=каширское":-2.0,"CITY\tnext=кирова":1.0,"CITY\tnext=колпакова":2.0,"CITY\tnext=ленинградская":-1.5,"CITY\tnext=ленинский":-0.5,"CITY\tnext=ленинского":2.0,"CITY\tnext=можайское":1.0,"CITY\tnext=москва":1.2,"CITY\tnext=новгород":3.5,"CITY\tnext=октябрьский":1.0,"CITY\tnext=он":-1.0,"CITY\tnext=ополчения":-1.0,"CITY\tnext=переулок":-1.0,"CITY\tnext=петербург":2.9,"CITY\tnext=пр":-2.8,"CITY\tnext=правды":0.9,"CITY\tnext=сампсониевский":-1.0,"CITY\tnext=северный":2.8,"CITY\tnext=т":-1.0,"CITY\tnext=ш":-0.9,"CITY\tnext=этаж":-1.0,"CITY\tnext=я":-1.9,"CITY\tnext=ямского":1.0,"CITY\tnext_kind=word":-0.2,"CITY\tposition=first":1.1,"CITY\tposition=last":-1.2,"CITY\tposition=middle":0.8,"CITY\tprefix1=а":-1.0,"CITY\tprefix1=б":-0.9,"CITY\tprefix1=в":1.7,"CITY\tprefix1=г":-1.0,"CITY\tprefix1=д":-0.2,"CITY\tprefix1=е":2.5,"CITY\tprefix1=з":-1.0,"CITY\tprefix1=и":-1.0,"CITY\tprefix1=к":0.1,"CITY\tprefix1=л":-1.0,"CITY\tprefix1=м":4.2,"CITY\tprefix1=н":1.4,"CITY\tprefix1=о":-1.0,"CITY\tprefix1=п":-0.9,"CITY\tprefix1=р":-0.4,"CITY\tprefix1=с":1.1,"CITY\tprefix1=т":2.0,"CITY\tprefix1=у":-1.0,"CITY\tprefix1=ш":-1.0,"CITY\tprefix1=э":-0.9,"CITY\tprefix1=я":-1.0,"CITY\tprefix2=а":-1.0,"CITY\tprefix2=б":-1.0,"CITY\tprefix2=ба":2.0,"CITY\tprefix2=бо":-1.0,"CITY\tprefix2=бр":-0.9,"CITY\tprefix2=ва":-1.0,"CITY\tprefix2=ви":3.6,"CITY\tprefix2=во":-0.9,"CITY\tprefix2=г":-1.0,"CITY\tprefix2=д":-1.0,"CITY\tprefix2=дм":-1.0,"CITY\tprefix2=до":1.8,"CITY\tprefix2=ер":2.5,"CITY\tprefix2=зе":-1.0,"CITY\tprefix2=ис":-1.0,"CITY\tprefix2=ка":1.9,"CITY\tprefix2=ки":1.0,"CITY\tprefix2=ко":-1.8,"CITY\tprefix2=ку":-1.0,"CITY\tprefix2=ле":-1.0,"CITY\tprefix2=ми":-0.6,"CITY\tprefix2=мо":2.8,"CITY\tprefix2=мы":2.0,"CITY\tprefix2=на":-2.6,"CITY\tprefix2=ни":3.5,"CITY\tprefix2=но":0.5,"CITY\tprefix2=оч":-1.0,"CITY\tprefix2=п":-1.0,"CITY\tprefix2=пе":2.1,"CITY\tprefix2=по":1.0,"CITY\tprefix2=пр":-3.0,"CITY\tprefix2=р":-1.0,"CITY\tprefix2=ре":1.6,"CITY\tprefix2=ро":-1.0,"CITY\tprefix2=с":-0.9,"CITY\tprefix2=са":1.9,"CITY\tprefix2=см":1.0,"CITY\tprefix2=со":-0.9,"CITY\tprefix2=то":2.0,"CITY\tprefix2=ул":-1.0,"CITY\tprefix2=ш":-1.0,"CITY\tprefix2=эн":-0.9,"CITY\tprefix2=яр":-1.0,"CITY\tprev=боровский":-0.1,"CITY\tprev=г":0.7,"CITY\tprev=зеленый":-1.0,"CITY\tprev=калужская":1.0,"CITY\tprev=кировская":0.9,"CITY\tprev=ленинский":1.0,"CITY\tprev=марьиной":-1.0,"CITY\tprev=москва":-10.5,"CITY\tprev=московская":2.8,"CITY\tprev=нижегородская":1.9,"CITY\tprev=нижний":3.0,"CITY\tprev=ордынка":-1.0,"CITY\tprev=санкт":3.8,"CITY\tprev=смоленск":-1.0,"CITY\tprev=ямского":-0.9,"CITY\tprev_kind=word":-0.4,"CITY\tsuffix1=а":2.6,"CITY\tsuffix1=б":-1.0,"CITY\tsuffix1=в":2.6,"CITY\tsuffix1=г":2.8,"CITY\tsuffix1=д":2.0,"CITY\tsuffix1=е":-0.1,"CITY\tsuffix1=и":3.0,"CITY\tsuffix1=й":-2.4,"CITY\tsuffix1=к":2.2,"CITY\tsuffix1=л":-1.0,"CITY\tsuffix1=м":-1.8,"CITY\tsuffix1=о":0.2,"CITY\tsuffix1=п":-1.0,"CITY\tsuffix1=р":-2.0,"CITY\tsuffix1=с":-0.9,"CITY\tsuffix1=т":1.9,"CITY\tsuffix1=ш":-1.0,"CITY\tsuffix1=ы":-1.0,"CITY\tsuffix1=я":-4.4,"CITY\tsuffix2=а":-1.0,"CITY\tsuffix2=ая":-2.5,"CITY\tsuffix2=б":-1.0,"CITY\tsuffix2=ва":1.3,"CITY\tsuffix2=во":0.3,"CITY\tsuffix2=г":-1.0,"CITY\tsuffix2=га":2.8,"CITY\tsuffix2=го":-1.0,"CITY\tsuffix2=д":-1.0,"CITY\tsuffix2=ий":0.6,"CITY\tsuffix2=ия":-1.0,"CITY\tsuffix2=ка":-1.0,"CITY\tsuffix2=кт":1.9,"CITY\tsuffix2=ля":-0.9,"CITY\tsuffix2=на":-0.9,"CITY\tsuffix2=но":0.9,"CITY\tsuffix2=ов":2.6,"CITY\tsuffix2=од":3.0,"CITY\tsuffix2=ое":-0.1,"CITY\tsuffix2=ой":-1.0,"CITY\tsuffix2=ок":-1.7,"CITY\tsuffix2=ом":-1.8,"CITY\tsuffix2=п":-1.0,"CITY\tsuffix2=пр":-1.0,"CITY\tsuffix2=р":-1.0,"CITY\tsuffix2=ра":-0.6,"CITY\tsuffix2=рг":3.8,"CITY\tsuffix2=ры":-1.0,"CITY\tsuffix2=с":-0.9,"CITY\tsuffix2=ск":3.9,"CITY\tsuffix2=ти":2.0,"CITY\tsuffix2=ул":-1.0,"CITY\tsuffix2=ха":2.0,"CITY\tsuffix2=ш":-1.0,"CITY\tsuffix2=щи":1.0,"CITY\tsuffix2=ый":-2.0,"CITY\tsuffix3=а":-1.0,"CITY\tsuffix3=б":-1.0,"CITY\tsuffix3=г":-1.0,"CITY\tsuffix3=д":-1.0,"CITY\tsuffix3=ект":-1.0,"CITY\tsuffix3=ина":-0.9,"CITY\tsuffix3=ино":0.9,"CITY\tsuffix3=ира":-0.6,"CITY\tsuffix3=иха":2.0,"CITY\tsuffix3=ищи":2.0,"CITY\tsuffix3=кая":-2.5,"CITY\tsuffix3=ква":4.1,"CITY\tsuffix3=кий":-2.9,"CITY\tsuffix3=кое":-3.7,"CITY\tsuffix3=ком":-1.8,"CITY\tsuffix3=кры":-1.0,"CITY\tsuffix3=лок":-1.7,"CITY\tsuffix3=ний":3.5,"CITY\tsuffix3=нка":-1.0,"CITY\tsuffix3=нкт":2.9,"CITY\tsuffix3=ное":3.6,"CITY\tsuffix3=нск":1.0,"CITY\tsuffix3=ный":-2.0,"CITY\tsuffix3=ова":-2.8,"CITY\tsuffix3=ово":0.3,"CITY\tsuffix3=ого":-1.0,"CITY\tsuffix3=оля":-0.9,"CITY\tsuffix3=ощи":-1.0,"CITY\tsuffix3=п":-1.0,"CITY\tsuffix3=пр":-1.0,"CITY\tsuffix3=р":-1.0,"CITY\tsuffix3=рия":-1.0,"CITY\tsuffix3=ров":2.8,"CITY\tsuffix3=род":3.0,"CITY\tsuffix3=рск":1.0,"CITY\tsuffix3=с":-0.9,"CITY\tsuffix3=сов":-0.9,"CITY\tsuffix3=тов":0.7,"CITY\tsuffix3=тти":2.0,"CITY\tsuffix3=уга":2.8,"CITY\tsuffix3=ул":-1.0,"CITY\tsuffix3=ург":3.8,"CITY\tsuffix3=ш":-1.0,"CITY\tsuffix3=шой":-1.0,"CITY\tsuffix3=ьск":1.9,"CITY\tword=а":-1.0,"CITY\tword=б":-1.0,"CITY\tword=балашиха":2.0,"CITY\tword=большой":-1.0,"CITY\tword=брюсов":-0.9,"CITY\tword=вавилова":-1.0,"CITY\tword=видное":3.6,"CITY\tword=воронина":-0.9,"CITY\tword=г":-1.0,"CITY\tword=д":-1.0,"CITY\tword=дмитрия":-1.0,"CITY\tword=домодедово":1.8,"CITY\tword=ермолино":2.5,"CITY\tword=зеленый":-1.0,"CITY\tword=искры":-1.0,"CITY\tword=калуга":2.8,"CITY\tword=каширское":-0.9,"CITY\tword=киров":2.8,"CITY\tword=кирова":-1.8,"CITY\tword=ком":-1.8,"CITY\tword=кутузовский":-1.0,"CITY\tword=ленинский":-1.0,"CITY\tword=мира":-0.6,"CITY\tword=можайское":-0.8,"CITY\tword=москва":4.1,"CITY\tword=московская":-0.5,"CITY\tword=мытищи":2.0,"CITY\tword=народного":-1.0,"CITY\tword=нахабино":-1.6,"CITY\tword=нижний":3.5,"CITY\tword=новгород":3.0,"CITY\tword=ново":-1.5,"CITY\tword=новоивановское":-1.0,"CITY\tword=очаковское":-1.0,"CITY\tword=п":-1.0,"CITY\tword=переулок":-1.7,"CITY\tword=петербург":3.8,"CITY\tword=подольск":1.9,"CITY\tword=поля":-0.9,"CITY\tword=пр":-1.0,"CITY\tword=пречистенка":-1.0,"CITY\tword=проспект":-1.0,"CITY\tword=р":-1.0,"CITY\tword=реутов":1.6,"CITY\tword=рощи":-1.0,"CITY\tword=с":-0.9,"CITY\tword=садовническая":-1.0,"CITY\tword=санкт":2.9,"CITY\tword=смоленск":1.0,"CITY\tword=сокольнический":-0.9,"CITY\tword=тольятти":2.0,"CITY\tword=ул":-1.0,"CITY\tword=ш":-1.0,"CITY\tword=электрогорск":1.0,"CITY\tword=электродный":-1.0,"CITY\tword=энтузиастов":-0.9,"CITY\tword=ярославская":-1.0,"DISTRICT\tbias":-0.8,"DISTRICT\tbos":-0.6,"DISTRICT\teos":-1.0,"DISTRICT\tkind=word":-0.8,"DISTRICT\tnext=большой":-1.0,"DISTRICT\tnext=видное":1.0,"DISTRICT\tnext=ермолино":0.9,"DISTRICT\tnext=колпакова":-1.0,"DISTRICT\tnext=комсомола":-1.0,"DISTRICT\tnext=нахабино":1.0,"DISTRICT\tnext=новгород":-0.9,"DISTRICT\tnext=новоивановское":1.6,"DISTRICT\tnext=пр":-0.6,"DISTRICT\tnext=р":1.0,"DISTRICT\tnext=северный":-0.8,"DISTRICT\tnext_kind=word":0.2,"DISTRICT\tposition=first":-0.6,"DISTRICT\tposition=last":-1.0,"DISTRICT\tposition=middle":0.8,"DISTRICT\tprefix1=б":1.9,"DISTRICT\tprefix1=д":-0.8,"DISTRICT\tprefix1=к":1.0,"DISTRICT\tprefix1=л":-0.6,"DISTRICT\tprefix1=м":-1.0,"DISTRICT\tprefix1=н":-0.9,"DISTRICT\tprefix1=о":1.6,"DISTRICT\tprefix1=п":-1.0,"DISTRICT\tprefix1=с":-1.0,"DISTRICT\tprefix2=бо":1.9,"DISTRICT\tprefix2=до":-0.8,"DISTRICT\tprefix2=кр":1.0,"DISTRICT\tprefix2=ле":-0.6,"DISTRICT\tprefix2=мы":-1.0,"DISTRICT\tprefix2=ни":-0.9,"DISTRICT\tprefix2=од":1.6,"DISTRICT\tprefix2=пе":-1.0,"DISTRICT\tprefix2=са":-1.0,"DISTRICT\tprev=большой":-1.0,"DISTRICT\tprev=калужская":0.9,"DISTRICT\tprev=московская":1.8,"DISTRICT\tprev=нижегородская":-0.9,"DISTRICT\tprev=санкт":-1.0,"DISTRICT\tprev_kind=word":-0.2,"DISTRICT\tsuffix1=г":-1.0,"DISTRICT\tsuffix1=и":-1.0,"DISTRICT\tsuffix1=й":3.0,"DISTRICT\tsuffix1=о":-1.8,"DISTRICT\tsuffix2=во":-0.8,"DISTRICT\tsuffix2=го":-1.0,"DISTRICT\tsuffix2=ий":3.0,"DISTRICT\tsuffix2=рг":-1.0,"DISTRICT\tsuffix2=щи":-1.0,"DISTRICT\tsuffix3=ищи":-1.0,"DISTRICT\tsuffix3=кий":3.9,"DISTRICT\tsuffix3=ний":-0.9,"DISTRICT\tsuffix3=ово":-0.8,"DISTRICT\tsuffix3=ого":-1.0,"DISTRICT\tsuffix3=ург":-1.0,"DISTRICT\tword=боровский":1.9,"DISTRICT\tword=домодедово":-0.8,"DISTRICT\tword=красногорский":1.0,"DISTRICT\tword=ленинградский":-0.6,"DISTRICT\tword=ленинский":1.0,"DISTRICT\tword=ленинского":-1.0,"DISTRICT\tword=мытищи":-1.0,"DISTRICT\tword=нижний":-0.9,"DISTRICT\tword=одинцовский":1.6,"DISTRICT\tword=петербург":-1.0,"DISTRICT\tword=сампсониевский":-1.0,"O\tbias":1.6,"O\tbos":0.1,"O\teos":0.9,"O\tkind=word":1.6,"O\tnext=б":1.4,"O\tnext=балашиха":-0.2,"O\tnext=г":3.0,"O\tnext=гагарина":-0.9,"O\tnext=гоголя":-1.0,"O\tnext=домодедово":-0.5,"O\tnext=й":-1.0,"O\tnext=каширское":2.0,"O\tnext=киров":-0.7,"O\tnext=комсомола":-1.0,"O\tnext=ленинского":-1.0,"O\tnext=марьиной":1.0,"O\tnext=москва":4.3,"O\tnext=мытищи":-0.6,"O\tnext=нижний":-1.0,"O\tnext=новгород":-1.0,"O\tnext=он":1.0,"O\tnext=переулок":1.0,"O\tnext=петербург":-0.9,"O\tnext=подольск":-0.3,"O\tnext=пр":0.9,"O\tnext=правды":-0.9,"O\tnext=реутов":-0.4,"O\tnext=рощи":-1.0,"O\tnext=с":-2.4,"O\tnext=т":1.0,"O\tnext=электрогорск":-0.1,"O\tnext=этаж":1.0,"O\tnext=ямского":-1.0,"O\tnext_kind=word":0.7,"O\tposition=first":0.1,"O\tposition=last":1.1,"O\tposition=middle":0.4,"O\tprefix1=а":2.0,"O\tprefix1=б":0.2,"O\tprefix1=в":-1.7,"O\tprefix1=г":1.1,"O\tprefix1=д":1.0,"O\tprefix1=е":-0.9,"O\tprefix1=з":-0.8,"O\tprefix1=и":-1.0,"O\tprefix1=й":-1.0,"O\tprefix1=к":-1.1,"O\tprefix1=л":-1.0,"O\tprefix1=м":-0.6,"O\tprefix1=н":-4.0,"O\tprefix1=о":-2.0,"O\tprefix1=п":3.8,"O\tprefix1=р":0.7,"O\tprefix1=т":-1.9,"O\tprefix1=у":3.0,"O\tprefix1=ш":3.9,"O\tprefix1=э":1.0,"O\tprefix1=я":0.9,"O\tprefix2=а":2.7,"O\tprefix2=ав":-0.7,"O\tprefix2=бр":-1.0,"O\tprefix2=бу":1.2,"O\tprefix2=ва":-1.6,"O\tprefix2=ви":-1.0,"O\tprefix2=вл":0.9,"O\tprefix2=г":3.0,"O\tprefix2=га":-0.9,"O\tprefix2=гр":-1.0,"O\tprefix2=д":1.0,"O\tprefix2=ер":-0.9,"O\tprefix2=зе":-0.8,"O\tprefix2=ис":-1.0,"O\tprefix2=й":-1.0,"O\tprefix2=к":1.0,"O\tprefix2=ка":-2.8,"O\tprefix2=ки":-0.7,"O\tprefix2=ко":1.4,"O\tprefix2=ле":-1.0,"O\tprefix2=ма":-1.0,"O\tprefix2=ми":-1.0,"O\tprefix2=мо":1.4,"O\tprefix2=ни":-2.0,"O\tprefix2=но":-2.0,"O\tprefix2=оз":-1.0,"O\tprefix2=ок":-1.0,"O\tprefix2=он":1.0,"O\tprefix2=ор":-1.0,"O\tprefix2=п":1.9,"O\tprefix2=пе":0.7,"O\tprefix2=пл":1.0,"O\tprefix2=по":-1.0,"O\tprefix2=пр":1.2,"O\tprefix2=р":1.7,"O\tprefix2=ро":-1.0,"O\tprefix2=с":1.9,"O\tprefix2=са":-0.9,"O\tprefix2=см":-1.0,"O\tprefix2=т":1.0,"O\tprefix2=та":-0.9,"O\tprefix2=то":-2.0,"O\tprefix2=ул":3.0,"O\tprefix2=ш":1.9,"O\tprefix2=шо":2.0,"O\tprefix2=эн":-1.0,"O\tprefix2=эт":2.0,"O\tprefix2=я":0.9,"O\tprev=а":2.7,"O\tprev=боровский":0.1,"O\tprev=бульвар":0.9,"O\tprev=г":2.3,"O\tprev=ермолино":-0.9,"O\tprev=зеленый":1.0,"O\tprev=земляной":-1.0,"O\tprev=калуга":-0.8,"O\tprev=каширское":1.9,"O\tprev=ком":2.4,"O\tprev=ленинского":-1.0,"O\tprev=марьиной":-1.0,"O\tprev=москва":6.5,"O\tprev=нижегородская":-1.0,"O\tprev=нижний":-1.0,"O\tprev=новоивановское":-1.0,"O\tprev=ордынка":-2.4,"O\tprev=песчаный":-0.9,"O\tprev=поля":-1.8,"O\tprev=проспект":1.0,"O\tprev=р":1.0,"O\tprev=рощи":-1.0,"O\tprev=санкт":-2.0,"O\tprev=сокольнический":-0.6,"O\tprev=ш":-0.9,"O\tprev=ямского":-1.0,"O\tprev_kind=word":1.5,"O\tsuffix1=а":-3.3,"O\tsuffix1=в":-2.0,"O\tsuffix1=г":1.0,"O\tsuffix1=д":1.9,"O\tsuffix1=е":-0.9,"O\tsuffix1=ж":2.0,"O\tsuffix1=и":-2.0,"O\tsuffix1=й":-5.8,"O\tsuffix1=к":1.8,"O\tsuffix1=л":1.4,"O\tsuffix1=м":2.4,"O\tsuffix1=н":1.0,"O\tsuffix1=о":-1.9,"O\tsuffix1=п":1.9,"O\tsuffix1=р":4.0,"O\tsuffix1=с":1.9,"O\tsuffix1=т":1.1,"O\tsuffix1=у":-1.0,"O\tsuffix1=ш":1.9,"O\tsuffix1=щ":0.9,"O\tsuffix1=ы":-2.9,"O\tsuffix1=ь":1.0,"O\tsuffix1=я":-2.8,"O\tsuffix2=а":2.7,"O\tsuffix2=ад":0.9,"O\tsuffix2=аж":2.0,"O\tsuffix2=ал":-1.6,"O\tsuffix2=ар":1.2,"O\tsuffix2=ау":-1.0,"O\tsuffix2=ая":-2.7,"O\tsuffix2=ва":-0.2,"O\tsuffix2=г":3.0,"O\tsuffix2=го":-1.0,"O\tsuffix2=д":1.0,"O\tsuffix2=ды":-1.9,"O\tsuffix2=дь":1.0,"O\tsuffix2=ер":-0.9,"O\tsuffix2=ещ":0.9,"O\tsuffix2=зд":1.0,"O\tsuffix2=ий":-3.0,"O\tsuffix2=й":-1.0,"O\tsuffix2=к":1.0,"O\tsuffix2=ка":-1.9,"O\tsuffix2=кт":0.1,"O\tsuffix2=ла":-1.0,"O\tsuffix2=ля":-1.0,"O\tsuffix2=на":-1.9,"O\tsuffix2=но":-0.9,"O\tsuffix2=ов":-2.0,"O\tsuffix2=од":-1.0,"O\tsuffix2=ое":-2.9,"O\tsuffix2=ой":-1.0,"O\tsuffix2=ок":2.7,"O\tsuffix2=ом":2.4,"O\tsuffix2=он":1.0,"O\tsuffix2=п":1.9,"O\tsuffix2=пр":2.0,"O\tsuffix2=р":1.7,"O\tsuffix2=ра":-1.0,"O\tsuffix2=рг":-2.0,"O\tsuffix2=ры":-1.0,"O\tsuffix2=с":1.9,"O\tsuffix2=се":2.0,"O\tsuffix2=ск":-1.9,"O\tsuffix2=т":1.0,"O\tsuffix2=ти":-1.0,"O\tsuffix2=ул":3.0,"O\tsuffix2=ш":1.9,"O\tsuffix2=щи":-1.0,"O\tsuffix2=ый":-0.8,"O\tsuffix2=я":0.9,"O\tsuffix3=а":2.7,"O\tsuffix3=адь":1.0,"O\tsuffix3=вал":-1.6,"O\tsuffix3=вар":1.2,"O\tsuffix3=вды":-1.9,"O\tsuffix3=г":3.0,"O\tsuffix3=д":1.0,"O\tsuffix3=езд":1.0,"O\tsuffix3=ект":1.0,"O\tsuffix3=ина":-1.9,"O\tsuffix3=ино":-0.9,"O\tsuffix3=ира":-1.0,"O\tsuffix3=й":-1.0,"O\tsuffix3=к":1.0,"O\tsuffix3=кая":-2.0,"O\tsuffix3=ква":-0.2,"O\tsuffix3=кий":-2.0,"O\tsuffix3=кое":-1.9,"O\tsuffix3=ком":2.4,"O\tsuffix3=кры":-1.0,"O\tsuffix3=лад":0.9,"O\tsuffix3=лок":2.7,"O\tsuffix3=мау":-1.0,"O\tsuffix3=мещ":0.9,"O\tsuffix3=ная":-0.7,"O\tsuffix3=ний":-1.0,"O\tsuffix3=нка":-1.9,"O\tsuffix3=нкт":-0.9,"O\tsuffix3=ное":-1.0,"O\tsuffix3=ной":-1.0,"O\tsuffix3=нск":-1.0,"O\tsuffix3=ный":-0.8,"O\tsuffix3=ого":-1.0,"O\tsuffix3=ола":-1.0,"O\tsuffix3=оля":-1.0,"O\tsuffix3=он":1.0,"O\tsuffix3=ощи":-1.0,"O\tsuffix3=п":1.9,"O\tsuffix3=пр":2.0,"O\tsuffix3=р":1.7,"O\tsuffix3=род":-1.0,"O\tsuffix3=с":1.9,"O\tsuffix3=сов":-1.0,"O\tsuffix3=ссе":2.0,"O\tsuffix3=т":1.0,"O\tsuffix3=таж":2.0,"O\tsuffix3=тов":-1.0,"O\tsuffix3=тти":-1.0,"O\tsuffix3=ул":3.0,"O\tsuffix3=ург":-2.0,"O\tsuffix3=ш":1.9,"O\tsuffix3=ьер":-0.9,"O\tsuffix3=ьск":-0.9,"O\tsuffix3=я":0.9,"O\tword=а":2.7,"O\tword=авиамоторная":-0.7,"O\tword=брюсов":-1.0,"O\tword=бульвар":1.2,"O\tword=вал":-1.6,"O\tword=видное":-1.0,"O\tword=влад":0.9,"O\tword=г":3.0,"O\tword=гагарина":-0.9,"O\tword=гримау":-1.0,"O\tword=д":1.0,"O\tword=ермолино":-0.9,"O\tword=зеленый":-0.8,"O\tword=искры":-1.0,"O\tword=й":-1.0,"O\tword=к":1.0,"O\tword=калинина":-1.0,"O\tword=карьер":-0.9,"O\tword=каширское":-0.9,"O\tword=кировская":-0.7,"O\tword=ком":2.4,"O\tword=комсомола":-1.0,"O\tword=ленинского":-1.0,"O\tword=марьиной":-1.0,"O\tword=мира":-1.0,"O\tword=можайское":-1.0,"O\tword=москва":-0.2,"O\tword=московская":2.6,"O\tword=нижегородская":-1.0,"O\tword=нижний":-1.0,"O\tword=новгород":-1.0,"O\tword=новомарьинская":-1.0,"O\tword=озерковская":-1.0,"O\tword=октябрьский":-1.0,"O\tword=он":1.0,"O\tword=ордынка":-1.0,"O\tword=п":1.9,"O\tword=переулок":2.7,"O\tword=петербург":-2.0,"O\tword=площадь":1.0,"O\tword=подольск":-0.9,"O\tword=поля":-1.0,"O\tword=помещ":0.9,"O\tword=пр":2.0,"O\tword=правды":-1.9,"O\tword=пречистенка":-0.9,"O\tword=проезд":1.0,"O\tword=проспект":1.0,"O\tword=р":1.7,"O\tword=рощи":-1.0,"O\tword=с":1.9,"O\tword=санкт":-0.9,"O\tword=смоленск":-1.0,"O\tword=т":1.0,"O\tword=тамбовская":-0.9,"O\tword=товарищеский":-1.0,"O\tword=тольятти":-1.0,"O\tword=ул":3.0,"O\tword=ш":1.9,"O\tword=шоссе":2.0,"O\tword=энтузиастов":-1.0,"O\tword=этаж":2.0,"O\tword=я":0.9,"REGION\tbias":-1.1,"REGION\tbos":-0.5,"REGION\teos":-3.5,"REGION\tkind=word":-1.1,"REGION\tnext=балашиха":0.2,"REGION\tnext=боровский":0.9,"REGION\tnext=домодедово":0.5,"REGION\tnext=калуга":1.0,"REGION\tnext=киров":1.6,"REGION\tnext=красногорский":1.0,"REGION\tnext=ленинский":1.3,"REGION\tnext=можайское":-1.0,"REGION\tnext=москва":-5.5,"REGION\tnext=мытищи":1.6,"REGION\tnext=нижний":1.6,"REGION\tnext=новгород":-0.6,"REGION\tnext=одинцовский":1.6,"REGION\tnext=петербург":-1.0,"REGION\tnext=подольск":0.3,"REGION\tnext=реутов":0.4,"REGION\tnext=рощи":-1.0,"REGION\tnext=сенная":-1.3,"REGION\tnext=смоленск":0.6,"REGION\tnext=тольятти":0.7,"REGION\tnext=шоссе":-0.6,"REGION\tnext=электрогорск":0.1,"REGION\tnext_kind=word":2.4,"REGION\tposition=first":-0.5,"REGION\tposition=middle":-0.6,"REGION\tprefix1=в":-0.6,"REGION\tprefix1=д":-1.0,"REGION\tprefix1=к":1.9,"REGION\tprefix1=м":-1.4,"REGION\tprefix1=н":1.0,"REGION\tprefix1=с":-1.0,"REGION\tprefix2=вя":-0.6,"REGION\tprefix2=де":-1.0,"REGION\tprefix2=ка":1.3,"REGION\tprefix2=ки":0.6,"REGION\tprefix2=ма":-1.0,"REGION\tprefix2=мо":-0.4,"REGION\tprefix2=ни":1.0,"REGION\tprefix2=са":-0.3,"REGION\tprefix2=см":-0.7,"REGION\tprev=ул":-0.6,"REGION\tprev_kind=word":-0.6,"REGION\tsuffix1=а":-1.0,"REGION\tsuffix1=в":-1.0,"REGION\tsuffix1=е":-0.6,"REGION\tsuffix1=й":-1.6,"REGION\tsuffix1=т":-1.0,"REGION\tsuffix1=я":4.1,"REGION\tsuffix2=ая":4.1,"REGION\tsuffix2=ва":-1.0,"REGION\tsuffix2=ий":-0.6,"REGION\tsuffix2=кт":-1.0,"REGION\tsuffix2=ов":-1.0,"REGION\tsuffix2=ое":-0.6,"REGION\tsuffix2=ой":-1.0,"REGION\tsuffix3=кая":4.1,"REGION\tsuffix3=ква":-1.0,"REGION\tsuffix3=кое":-0.6,"REGION\tsuffix3=ний":-0.6,"REGION\tsuffix3=нкт":-1.0,"REGION\tsuffix3=ной":-1.0,"REGION\tsuffix3=ров":-1.0,"REGION\tword=вятская":-0.6,"REGION\tword=дербеневская":-1.0,"REGION\tword=калужская":1.9,"REGION\tword=каширское":-0.6,"REGION\tword=киров":-1.0,"REGION\tword=кировская":1.6,"REGION\tword=марьиной":-1.0,"REGION\tword=москва":-1.0,"REGION\tword=московская":0.6,"REGION\tword=нижегородская":1.6,"REGION\tword=нижний":-0.6,"REGION\tword=самарская":0.7,"REGION\tword=санкт":-1.0,"REGION\tword=смоленская":-0.7,"SETTLEMENT\tbias":-1.2,"SETTLEMENT\tbos":-0.4,"SETTLEMENT\teos":-0.3,"SETTLEMENT\tkind=word":-1.2,"SETTLEMENT\tnext=вокзальная":2.0,"SETTLEMENT\tnext=гагарина":-1.0,"SETTLEMENT\tnext=калинина":2.6,"SETTLEMENT\tnext=каширское":1.9,"SETTLEMENT\tnext=кирова":-1.0,"SETTLEMENT\tnext=ленинградская":-1.9,"SETTLEMENT\tnext=нижний":-0.6,"SETTLEMENT\tnext=пр":-0.9,"SETTLEMENT\tnext=сампсониевский":-1.0,"SETTLEMENT\tnext=ш":-1.0,"SETTLEMENT\tnext_kind=word":-0.9,"SETTLEMENT\tposition=first":-0.4,"SETTLEMENT\tposition=last":-1.4,"SETTLEMENT\tposition=middle":0.6,"SETTLEMENT\tprefix1=б":-1.0,"SETTLEMENT\tprefix1=в":-0.7,"SETTLEMENT\tprefix1=е":-1.0,"SETTLEMENT\tprefix1=к":-2.0,"SETTLEMENT\tprefix1=м":-1.0,"SETTLEMENT\tprefix1=н":3.1,"SETTLEMENT\tprefix1=р":-0.3,"SETTLEMENT\tprefix1=с":2.7,"SETTLEMENT\tprefix1=э":-1.0,"SETTLEMENT\tprefix2=бо":-1.0,"SETTLEMENT\tprefix2=ви":-0.7,"SETTLEMENT\tprefix2=ер":-1.0,"SETTLEMENT\tprefix2=ка":-1.0,"SETTLEMENT\tprefix2=ко":-1.0,"SETTLEMENT\tprefix2=мо":-1.0,"SETTLEMENT\tprefix2=на":2.1,"SETTLEMENT\tprefix2=ни":-0.6,"SETTLEMENT\tprefix2=но":1.6,"SETTLEMENT\tprefix2=ре":-0.3,"SETTLEMENT\tprefix2=се":2.7,"SETTLEMENT\tprefix2=эт":-1.0,"SETTLEMENT\tprev=балашиха":-0.4,"SETTLEMENT\tprev=домодедово":0.9,"SETTLEMENT\tprev=красногорский":1.0,"SETTLEMENT\tprev=москва":-1.0,"SETTLEMENT\tprev=нижний":-1.0,"SETTLEMENT\tprev=одинцовский":1.6,"SETTLEMENT\tprev=петербург":-1.0,"SETTLEMENT\tprev=смоленск":-0.9,"SETTLEMENT\tprev_kind=word":-0.8,"SETTLEMENT\tsuffix1=а":-1.0,"SETTLEMENT\tsuffix1=д":-1.0,"SETTLEMENT\tsuffix1=е":1.8,"SETTLEMENT\tsuffix1=ж":-1.0,"SETTLEMENT\tsuffix1=о":0.6,"SETTLEMENT\tsuffix1=я":-0.6,"SETTLEMENT\tsuffix2=аж":-1.0,"SETTLEMENT\tsuffix2=ая":-0.6,"SETTLEMENT\tsuffix2=ва":-1.0,"SETTLEMENT\tsuffix2=во":-1.9,"SETTLEMENT\tsuffix2=но":2.5,"SETTLEMENT\tsuffix2=од":-1.0,"SETTLEMENT\tsuffix2=ое":1.8,"SETTLEMENT\tsuffix2=ой":-1.0,"SETTLEMENT\tsuffix2=ый":1.0,"SETTLEMENT\tsuffix3=ино":2.5,"SETTLEMENT\tsuffix3=кая":-0.6,"SETTLEMENT\tsuffix3=кое":2.5,"SETTLEMENT\tsuffix3=ное":-0.7,"SETTLEMENT\tsuffix3=ный":1.0,"SETTLEMENT\tsuffix3=ова":-1.0,"SETTLEMENT\tsuffix3=ово":-1.9,"SETTLEMENT\tsuffix3=род":-1.0,"SETTLEMENT\tsuffix3=таж":-1.0,"SETTLEMENT\tsuffix3=шой":-1.0,"SETTLEMENT\tword=большой":-1.0,"SETTLEMENT\tword=видное":-0.7,"SETTLEMENT\tword=ермолино":-1.0,"SETTLEMENT\tword=каширское":-1.0,"SETTLEMENT\tword=колпакова":-1.0,"SETTLEMENT\tword=можайское":-1.0,"SETTLEMENT\tword=научный":-1.4,"SETTLEMENT\tword=нахабино":3.5,"SETTLEMENT\tword=нижегородская":-0.6,"SETTLEMENT\tword=новгород":-1.0,"SETTLEMENT\tword=ново":-1.9,"SETTLEMENT\tword=новоивановское":4.5,"SETTLEMENT\tword=революционный":-0.3,"SETTLEMENT\tword=северный":2.7,"SETTLEMENT\tword=этаж":-1.0,"STREET\tbias":0.8,"STREET\tbos":0.3,"STREET\teos":3.0,"STREET\tkind=word":0.8,"STREET\tnext=б":0.4,"STREET\tnext=боровский":-0.9,"STREET\tnext=буденного":-1.0,"STREET\tnext=вал":0.9,"STREET\tnext=вокзальная":-1.0,"STREET\tnext=володарского":-0.9,"STREET\tnext=воронина":-1.0,"STREET\tnext=донского":1.0,"STREET\tnext=ермолино":-0.9,"STREET\tnext=й":2.0,"STREET\tnext=калинина":-1.6,"STREET\tnext=калуга":-1.0,"STREET\tnext=каширское":-1.9,"STREET\tnext=киров":-0.9,"STREET\tnext=колпакова":-1.0,"STREET\tnext=комсомола":2.0,"STREET\tnext=красногорский":-1.0,"STREET\tnext=ленинградская":3.4,"STREET\tnext=ленинский":-0.8,"STREET\tnext=ленинского":-1.0,"STREET\tnext=марьиной":-1.0,"STREET\tnext=мытищи":-1.0,"STREET\tnext=нахабино":-1.0,"STREET\tnext=новгород":-1.0,"STREET\tnext=новоивановское":-1.6,"STREET\tnext=одинцовский":-1.6,"STREET\tnext=октябрьский":-1.0,"STREET\tnext=ополчения":1.0,"STREET\tnext=петербург":-1.0,"STREET\tnext=пр":3.4,"STREET\tnext=р":-1.0,"STREET\tnext=рощи":2.0,"STREET\tnext=с":2.4,"STREET\tnext=сампсониевский":2.0,"STREET\tnext=северный":-2.0,"STREET\tnext=сенная":1.3,"STREET\tnext=смоленск":-0.6,"STREET\tnext=тольятти":-0.7,"STREET\tnext=ш":1.9,"STREET\tnext=шоссе":0.6,"STREET\tnext=я":1.9,"STREET\tnext_kind=word":-2.2,"STREET\tposition=first":0.3,"STREET\tposition=last":2.5,"STREET\tposition=middle":-2.0,"STREET\tprefix1=а":-1.0,"STREET\tprefix1=б":-0.2,"STREET\tprefix1=в":1.3,"STREET\tprefix1=г":-0.1,"STREET\tprefix1=д":1.0,"STREET\tprefix1=е":-0.6,"STREET\tprefix1=з":1.8,"STREET\tprefix1=и":2.0,"STREET\tprefix1=й":1.0,"STREET\tprefix1=к":0.1,"STREET\tprefix1=л":2.6,"STREET\tprefix1=м":-0.2,"STREET\tprefix1=н":-0.6,"STREET\tprefix1=о":1.4,"STREET\tprefix1=п":-1.9,"STREET\tprefix1=с":-1.8,"STREET\tprefix1=т":-0.1,"STREET\tprefix1=у":-2.0,"STREET\tprefix1=ш":-2.9,"STREET\tprefix1=э":0.9,"STREET\tprefix1=я":0.1,"STREET\tprefix2=а":-1.7,"STREET\tprefix2=ав":0.7,"STREET\tprefix2=б":1.0,"STREET\tprefix2=ба":-2.0,"STREET\tprefix2=бо":0.1,"STREET\tprefix2=бр":1.9,"STREET\tprefix2=бу":-1.2,"STREET\tprefix2=ва":2.6,"STREET\tprefix2=ви":-1.9,"STREET\tprefix2=вл":-0.9,"STREET\tprefix2=во":0.9,"STREET\tprefix2=вя":0.6,"STREET\tprefix2=г":-2.0,"STREET\tprefix2=га":0.9,"STREET\tprefix2=гр":1.0,"STREET\tprefix2=де":1.0,"STREET\tprefix2=дм":1.0,"STREET\tprefix2=до":-1.0,"STREET\tprefix2=ер":-0.6,"STREET\tprefix2=зе":1.8,"STREET\tprefix2=ис":2.0,"STREET\tprefix2=й":1.0,"STREET\tprefix2=к":-1.0,"STREET\tprefix2=ка":0.6,"STREET\tprefix2=ки":-0.9,"STREET\tprefix2=ко":1.4,"STREET\tprefix2=кр":-1.0,"STREET\tprefix2=ку":1.0,"STREET\tprefix2=ле":2.6,"STREET\tprefix2=ма":2.0,"STREET\tprefix2=ми":1.6,"STREET\tprefix2=мо":-2.8,"STREET\tprefix2=мы":-1.0,"STREET\tprefix2=на":0.5,"STREET\tprefix2=ни":-1.0,"STREET\tprefix2=но":-0.1,"STREET\tprefix2=од":-1.6,"STREET\tprefix2=оз":1.0,"STREET\tprefix2=ок":1.0,"STREET\tprefix2=он":-1.0,"STREET\tprefix2=ор":1.0,"STREET\tprefix2=оч":1.0,"STREET\tprefix2=п":-0.9,"STREET\tprefix2=пе":-1.8,"STREET\tprefix2=пл":-1.0,"STREET\tprefix2=пр":1.8,"STREET\tprefix2=р":-0.7,"STREET\tprefix2=ре":-1.3,"STREET\tprefix2=ро":2.0,"STREET\tprefix2=с":-1.0,"STREET\tprefix2=са":0.3,"STREET\tprefix2=се":-2.7,"STREET\tprefix2=см":0.7,"STREET\tprefix2=со":0.9,"STREET\tprefix2=т":-1.0,"STREET\tprefix2=та":0.9,"STREET\tprefix2=ул":-2.0,"STREET\tprefix2=ш":-0.9,"STREET\tprefix2=шо":-2.0,"STREET\tprefix2=эн":1.9,"STREET\tprefix2=эт":-1.0,"STREET\tprefix2=я":-0.9,"STREET\tprefix2=яр":1.0,"STREET\tprev=а":-2.7,"STREET\tprev=балашиха":0.4,"STREET\tprev=большой":1.0,"STREET\tprev=бульвар":-0.9,"STREET\tprev=г":-3.0,"STREET\tprev=домодедово":-0.9,"STREET\tprev=ермолино":0.9,"STREET\tprev=земляной":1.0,"STREET\tprev=калуга":0.8,"STREET\tprev=калужская":-1.9,"STREET\tprev=каширское":-1.9,"STREET\tprev=кировская":-0.9,"STREET\tprev=ком":-2.4,"STREET\tprev=красногорский":-1.0,"STREET\tprev=ленинский":-1.0,"STREET\tprev=ленинского":1.0,"STREET\tprev=марьиной":2.0,"STREET\tprev=москва":5.0,"STREET\tprev=московская":-4.6,"STREET\tprev=нижний":-1.0,"STREET\tprev=новоивановское":1.0,"STREET\tprev=одинцовский":-1.6,"STREET\tprev=ордынка":3.4,"STREET\tprev=песчаный":0.9,"STREET\tprev=петербург":1.0,"STREET\tprev=поля":1.8,"STREET\tprev=проспект":-1.0,"STREET\tprev=р":-1.0,"STREET\tprev=рощи":1.0,"STREET\tprev=санкт":-0.8,"STREET\tprev=смоленск":1.9,"STREET\tprev=сокольнический":0.6,"STREET\tprev=ул":0.6,"STREET\tprev=ш":0.9,"STREET\tprev=ямского":1.9,"STREET\tprev_kind=word":0.5,"STREET\tsuffix1=а":2.7,"STREET\tsuffix1=б":1.0,"STREET\tsuffix1=в":0.4,"STREET\tsuffix1=г":-2.8,"STREET\tsuffix1=д":-2.9,"STREET\tsuffix1=е":-0.2,"STREET\tsuffix1=ж":-1.0,"STREET\tsuffix1=й":6.8,"STREET\tsuffix1=к":-4.0,"STREET\tsuffix1=л":-0.4,"STREET\tsuffix1=м":-0.6,"STREET\tsuffix1=н":-1.0,"STREET\tsuffix1=о":2.9,"STREET\tsuffix1=п":-0.9,"STREET\tsuffix1=р":-2.0,"STREET\tsuffix1=с":-1.0,"STREET\tsuffix1=т":-2.0,"STREET\tsuffix1=у":1.0,"STREET\tsuffix1=ш":-0.9,"STREET\tsuffix1=щ":-0.9,"STREET\tsuffix1=ы":3.9,"STREET\tsuffix1=ь":-1.0,"STREET\tsuffix1=я":3.7,"STREET\tsuffix2=а":-1.7,"STREET\tsuffix2=ад":-0.9,"STREET\tsuffix2=аж":-1.0,"STREET\tsuffix2=ал":1.6,"STREET\tsuffix2=ар":-1.2,"STREET\tsuffix2=ау":1.0,"STREET\tsuffix2=ая":1.7,"STREET\tsuffix2=б":1.0,"STREET\tsuffix2=ва":0.9,"STREET\tsuffix2=во":2.4,"STREET\tsuffix2=г":-2.0,"STREET\tsuffix2=га":-2.8,"STREET\tsuffix2=го":3.0,"STREET\tsuffix2=ды":1.9,"STREET\tsuffix2=дь":-1.0,"STREET\tsuffix2=ер":0.9,"STREET\tsuffix2=ещ":-0.9,"STREET\tsuffix2=зд":-1.0,"STREET\tsuffix2=ия":1.0,"STREET\tsuffix2=й":1.0,"STREET\tsuffix2=к":-1.0,"STREET\tsuffix2=ка":2.9,"STREET\tsuffix2=кт":-1.0,"STREET\tsuffix2=ла":1.0,"STREET\tsuffix2=ля":1.9,"STREET\tsuffix2=на":2.8,"STREET\tsuffix2=но":-2.5,"STREET\tsuffix2=ов":0.4,"STREET\tsuffix2=од":-1.0,"STREET\tsuffix2=ое":1.8,"STREET\tsuffix2=ой":4.0,"STREET\tsuffix2=ок":-1.0,"STREET\tsuffix2=ом":-0.6,"STREET\tsuffix2=он":-1.0,"STREET\tsuffix2=п":-0.9,"STREET\tsuffix2=пр":-1.0,"STREET\tsuffix2=р":-0.7,"STREET\tsuffix2=ра":1.6,"STREET\tsuffix2=рг":-0.8,"STREET\tsuffix2=ры":2.0,"STREET\tsuffix2=с":-1.0,"STREET\tsuffix2=се":-2.0,"STREET\tsuffix2=ск":-2.0,"STREET\tsuffix2=т":-1.0,"STREET\tsuffix2=ти":-1.0,"STREET\tsuffix2=ул":-2.0,"STREET\tsuffix2=ха":-2.0,"STREET\tsuffix2=ш":-0.9,"STREET\tsuffix2=щи":1.0,"STREET\tsuffix2=ый":1.8,"STREET\tsuffix2=я":-0.9,"STREET\tsuffix3=а":-1.7,"STREET\tsuffix3=адь":-1.0,"STREET\tsuffix3=б":1.0,"STREET\tsuffix3=вал":1.6,"STREET\tsuffix3=вар":-1.2,"STREET\tsuffix3=вды":1.9,"STREET\tsuffix3=г":-2.0,"STREET\tsuffix3=езд":-1.0,"STREET\tsuffix3=ина":2.8,"STREET\tsuffix3=ино":-2.5,"STREET\tsuffix3=ира":1.6,"STREET\tsuffix3=иха":-2.0,"STREET\tsuffix3=ищи":-1.0,"STREET\tsuffix3=й":1.0,"STREET\tsuffix3=к":-1.0,"STREET\tsuffix3=кая":1.0,"STREET\tsuffix3=ква":-2.9,"STREET\tsuffix3=кий":1.0,"STREET\tsuffix3=кое":3.7,"STREET\tsuffix3=ком":-0.6,"STREET\tsuffix3=кры":2.0,"STREET\tsuffix3=лад":-0.9,"STREET\tsuffix3=лок":-1.0,"STREET\tsuffix3=мау":1.0,"STREET\tsuffix3=мещ":-0.9,"STREET\tsuffix3=ная":0.7,"STREET\tsuffix3=ний":-1.0,"STREET\tsuffix3=нка":2.9,"STREET\tsuffix3=нкт":-1.0,"STREET\tsuffix3=ное":-1.9,"STREET\tsuffix3=ной":2.0,"STREET\tsuffix3=ный":1.8,"STREET\tsuffix3=ова":3.8,"STREET\tsuffix3=ово":2.4,"STREET\tsuffix3=ого":3.0,"STREET\tsuffix3=ола":1.0,"STREET\tsuffix3=оля":1.9,"STREET\tsuffix3=он":-1.0,"STREET\tsuffix3=ощи":2.0,"STREET\tsuffix3=п":-0.9,"STREET\tsuffix3=пр":-1.0,"STREET\tsuffix3=р":-0.7,"STREET\tsuffix3=рия":1.0,"STREET\tsuffix3=ров":-1.8,"STREET\tsuffix3=род":-1.0,"STREET\tsuffix3=рск":-1.0,"STREET\tsuffix3=с":-1.0,"STREET\tsuffix3=сов":1.9,"STREET\tsuffix3=ссе":-2.0,"STREET\tsuffix3=т":-1.0,"STREET\tsuffix3=таж":-1.0,"STREET\tsuffix3=тов":0.3,"STREET\tsuffix3=тти":-1.0,"STREET\tsuffix3=уга":-2.8,"STREET\tsuffix3=ул":-2.0,"STREET\tsuffix3=ург":-0.8,"STREET\tsuffix3=ш":-0.9,"STREET\tsuffix3=шой":2.0,"STREET\tsuffix3=ьер":0.9,"STREET\tsuffix3=ьск":-1.0,"STREET\tsuffix3=я":-0.9,"STREET\tword=а":-1.7,"STREET\tword=авиамоторная":0.7,"STREET\tword=б":1.0,"STREET\tword=балашиха":-2.0,"STREET\tword=большой":2.0,"STREET\tword=боровский":-1.9,"STREET\tword=брюсов":1.9,"STREET\tword=бульвар":-1.2,"STREET\tword=вавилова":1.0,"STREET\tword=вал":1.6,"STREET\tword=видное":-1.9,"STREET\tword=влад":-0.9,"STREET\tword=воронина":0.9,"STREET\tword=вятская":0.6,"STREET\tword=г":-2.0,"STREET\tword=гагарина":0.9,"STREET\tword=гримау":1.0,"STREET\tword=дербеневская":1.0,"STREET\tword=дмитрия":1.0,"STREET\tword=домодедово":-1.0,"STREET\tword=ермолино":-0.6,"STREET\tword=зеленый":1.8,"STREET\tword=искры":2.0,"STREET\tword=й":1.0,"STREET\tword=к":-1.0,"STREET\tword=калинина":1.0,"STREET\tword=калуга":-2.8,"STREET\tword=калужская":-1.9,"STREET\tword=карьер":0.9,"STREET\tword=каширское":3.4,"STREET\tword=киров":-1.8,"STREET\tword=кирова":1.8,"STREET\tword=кировская":-0.9,"STREET\tword=колпакова":1.0,"STREET\tword=ком":-0.6,"STREET\tword=комсомола":1.0,"STREET\tword=красногорский":-1.0,"STREET\tword=кутузовский":1.0,"STREET\tword=ленинградский":0.6,"STREET\tword=ленинского":2.0,"STREET\tword=марьиной":2.0,"STREET\tword=мира":1.6,"STREET\tword=можайское":2.8,"STREET\tword=москва":-2.9,"STREET\tword=московская":-2.7,"STREET\tword=мытищи":-1.0,"STREET\tword=народного":1.0,"STREET\tword=научный":1.4,"STREET\tword=нахабино":-1.9,"STREET\tword=нижний":-1.0,"STREET\tword=новгород":-1.0,"STREET\tword=ново":3.4,"STREET\tword=новоивановское":-3.5,"STREET\tword=новомарьинская":1.0,"STREET\tword=одинцовский":-1.6,"STREET\tword=озерковская":1.0,"STREET\tword=октябрьский":1.0,"STREET\tword=он":-1.0,"STREET\tword=ордынка":1.0,"STREET\tword=очаковское":1.0,"STREET\tword=п":-0.9,"STREET\tword=переулок":-1.0,"STREET\tword=петербург":-0.8,"STREET\tword=площадь":-1.0,"STREET\tword=подольск":-1.0,"STREET\tword=поля":1.9,"STREET\tword=помещ":-0.9,"STREET\tword=пр":-1.0,"STREET\tword=правды":1.9,"STREET\tword=пречистенка":1.9,"STREET\tword=проезд":-1.0,"STREET\tword=р":-0.7,"STREET\tword=революционный":0.3,"STREET\tword=реутов":-1.6,"STREET\tword=рощи":2.0,"STREET\tword=с":-1.0,"STREET\tword=садовническая":1.0,"STREET\tword=самарская":-0.7,"STREET\tword=сампсониевский":1.0,"STREET\tword=санкт":-1.0,"STREET\tword=северный":-2.7,"STREET\tword=смоленская":0.7,"STREET\tword=сокольнический":0.9,"STREET\tword=т":-1.0,"STREET\tword=тамбовская":0.9,"STREET\tword=товарищеский":1.0,"STREET\tword=тольятти":-1.0,"STREET\tword=ул":-2.0,"STREET\tword=ш":-0.9,"STREET\tword=шоссе":-2.0,"STREET\tword=электрогорск":-1.0,"STREET\tword=электродный":1.0,"STREET\tword=энтузиастов":1.9,"STREET\tword=этаж":-1.0,"STREET\tword=я":-0.9,"STREET\tword=ярославская":1.0},"transitions":{"\tCITY":1.1,"\tDISTRICT":-0.6,"\tO":0.1,"\tREGION":-0.5,"\tSETTLEMENT":-0.4,"\tSTREET":0.3,"CITY\t":0.9,"CITY\tCITY":-2.6,"CITY\tDISTRICT":-0.5,"CITY\tO":0.3,"CITY\tSETTLEMENT":0.4,"CITY\tSTREET":2.2,"DISTRICT\t":-1.0,"DISTRICT\tCITY":1.0,"DISTRICT\tO":-0.6,"DISTRICT\tSETTLEMENT":0.8,"DISTRICT\tSTREET":-1.0,"O\t":0.9,"O\tCITY":1.8,"O\tO":2.6,"O\tREGION":-0.6,"O\tSTREET":-3.1,"REGION\t":-3.5,"REGION\tCITY":4.2,"REGION\tDISTRICT":3.1,"REGION\tO":-0.6,"REGION\tSETTLEMENT":-1.0,"REGION\tSTREET":-3.3,"SETTLEMENT\t":-0.3,"SETTLEMENT\tCITY":-0.6,"SETTLEMENT\tDISTRICT":-1.0,"SETTLEMENT\tO":-1.9,"SETTLEMENT\tSTREET":2.6,"STREET\t":3.0,"STREET\tCITY":-4.2,"STREET\tDISTRICT":-1.8,"STREET\tO":1.7,"STREET\tSETTLEMENT":-1.0,"STREET\tSTREET":3.1}} \ No newline at end of file +{"format":"address-normalizer-compact-sequence-v1","training":{"algorithm":"epoch-averaged structured perceptron","seed":2017,"epochs":10,"examples":359,"source":"source-verifiable legacy reference rows with deterministic marker-free views","dataset":"benchmarks/legacy_500.jsonl","dataset_sha256":"853916e36cfc5a52d06add0524ba64ca2c771073c1c4e98cd5645116e7f74588","split":"SHA-256 by canonical address group: 70/15/15"},"labels":["O","REGION","DISTRICT","CITY","SETTLEMENT","STREET"],"emissions":{"CITY\tbias":0.7,"CITY\tbos":1.1,"CITY\teos":0.9,"CITY\tkind=word":0.7,"CITY\tnext=б":-1.8,"CITY\tnext=большой":1.0,"CITY\tnext=буденного":1.0,"CITY\tnext=вал":-0.9,"CITY\tnext=видное":-1.0,"CITY\tnext=вокзальная":-1.0,"CITY\tnext=володарского":0.9,"CITY\tnext=воронина":1.0,"CITY\tnext=г":-3.0,"CITY\tnext=гагарина":1.9,"CITY\tnext=гоголя":1.0,"CITY\tnext=донского":-1.0,"CITY\tnext=й":-1.0,"CITY\tnext=калинина":-1.0,"CITY\tnext=каширское":-2.0,"CITY\tnext=кирова":1.0,"CITY\tnext=колпакова":2.0,"CITY\tnext=ленинградская":-1.5,"CITY\tnext=ленинский":-0.5,"CITY\tnext=ленинского":2.0,"CITY\tnext=можайское":1.0,"CITY\tnext=москва":1.2,"CITY\tnext=новгород":3.5,"CITY\tnext=октябрьский":1.0,"CITY\tnext=он":-1.0,"CITY\tnext=ополчения":-1.0,"CITY\tnext=переулок":-1.0,"CITY\tnext=петербург":2.9,"CITY\tnext=пр":-2.8,"CITY\tnext=правды":0.9,"CITY\tnext=сампсониевский":-1.0,"CITY\tnext=северный":2.8,"CITY\tnext=т":-1.0,"CITY\tnext=ш":-0.9,"CITY\tnext=этаж":-1.0,"CITY\tnext=я":-1.9,"CITY\tnext=ямского":1.0,"CITY\tnext_kind=word":-0.2,"CITY\tposition=first":1.1,"CITY\tposition=last":-1.2,"CITY\tposition=middle":0.8,"CITY\tprefix1=а":-1.0,"CITY\tprefix1=б":-0.9,"CITY\tprefix1=в":1.7,"CITY\tprefix1=г":-1.0,"CITY\tprefix1=д":-0.2,"CITY\tprefix1=е":2.5,"CITY\tprefix1=з":-1.0,"CITY\tprefix1=и":-1.0,"CITY\tprefix1=к":0.1,"CITY\tprefix1=л":-1.0,"CITY\tprefix1=м":4.2,"CITY\tprefix1=н":1.4,"CITY\tprefix1=о":-1.0,"CITY\tprefix1=п":-0.9,"CITY\tprefix1=р":-0.4,"CITY\tprefix1=с":1.1,"CITY\tprefix1=т":2.0,"CITY\tprefix1=у":-1.0,"CITY\tprefix1=ш":-1.0,"CITY\tprefix1=э":-0.9,"CITY\tprefix1=я":-1.0,"CITY\tprefix2=а":-1.0,"CITY\tprefix2=б":-1.0,"CITY\tprefix2=ба":2.0,"CITY\tprefix2=бо":-1.0,"CITY\tprefix2=бр":-0.9,"CITY\tprefix2=ва":-1.0,"CITY\tprefix2=ви":3.6,"CITY\tprefix2=во":-0.9,"CITY\tprefix2=г":-1.0,"CITY\tprefix2=д":-1.0,"CITY\tprefix2=дм":-1.0,"CITY\tprefix2=до":1.8,"CITY\tprefix2=ер":2.5,"CITY\tprefix2=зе":-1.0,"CITY\tprefix2=ис":-1.0,"CITY\tprefix2=ка":1.9,"CITY\tprefix2=ки":1.0,"CITY\tprefix2=ко":-1.8,"CITY\tprefix2=ку":-1.0,"CITY\tprefix2=ле":-1.0,"CITY\tprefix2=ми":-0.6,"CITY\tprefix2=мо":2.8,"CITY\tprefix2=мы":2.0,"CITY\tprefix2=на":-2.6,"CITY\tprefix2=ни":3.5,"CITY\tprefix2=но":0.5,"CITY\tprefix2=оч":-1.0,"CITY\tprefix2=п":-1.0,"CITY\tprefix2=пе":2.1,"CITY\tprefix2=по":1.0,"CITY\tprefix2=пр":-3.0,"CITY\tprefix2=р":-1.0,"CITY\tprefix2=ре":1.6,"CITY\tprefix2=ро":-1.0,"CITY\tprefix2=с":-0.9,"CITY\tprefix2=са":1.9,"CITY\tprefix2=см":1.0,"CITY\tprefix2=со":-0.9,"CITY\tprefix2=то":2.0,"CITY\tprefix2=ул":-1.0,"CITY\tprefix2=ш":-1.0,"CITY\tprefix2=эн":-0.9,"CITY\tprefix2=яр":-1.0,"CITY\tprev=боровский":-0.1,"CITY\tprev=г":0.7,"CITY\tprev=зеленый":-1.0,"CITY\tprev=калужская":1.0,"CITY\tprev=кировская":0.9,"CITY\tprev=ленинский":1.0,"CITY\tprev=марьиной":-1.0,"CITY\tprev=москва":-10.5,"CITY\tprev=московская":2.8,"CITY\tprev=нижегородская":1.9,"CITY\tprev=нижний":3.0,"CITY\tprev=ордынка":-1.0,"CITY\tprev=санкт":3.8,"CITY\tprev=смоленск":-1.0,"CITY\tprev=ямского":-0.9,"CITY\tprev_kind=word":-0.4,"CITY\tsuffix1=а":2.6,"CITY\tsuffix1=б":-1.0,"CITY\tsuffix1=в":2.6,"CITY\tsuffix1=г":2.8,"CITY\tsuffix1=д":2.0,"CITY\tsuffix1=е":-0.1,"CITY\tsuffix1=и":3.0,"CITY\tsuffix1=й":-2.4,"CITY\tsuffix1=к":2.2,"CITY\tsuffix1=л":-1.0,"CITY\tsuffix1=м":-1.8,"CITY\tsuffix1=о":0.2,"CITY\tsuffix1=п":-1.0,"CITY\tsuffix1=р":-2.0,"CITY\tsuffix1=с":-0.9,"CITY\tsuffix1=т":1.9,"CITY\tsuffix1=ш":-1.0,"CITY\tsuffix1=ы":-1.0,"CITY\tsuffix1=я":-4.4,"CITY\tsuffix2=а":-1.0,"CITY\tsuffix2=ая":-2.5,"CITY\tsuffix2=б":-1.0,"CITY\tsuffix2=ва":1.3,"CITY\tsuffix2=во":0.3,"CITY\tsuffix2=г":-1.0,"CITY\tsuffix2=га":2.8,"CITY\tsuffix2=го":-1.0,"CITY\tsuffix2=д":-1.0,"CITY\tsuffix2=ий":0.6,"CITY\tsuffix2=ия":-1.0,"CITY\tsuffix2=ка":-1.0,"CITY\tsuffix2=кт":1.9,"CITY\tsuffix2=ля":-0.9,"CITY\tsuffix2=на":-0.9,"CITY\tsuffix2=но":0.9,"CITY\tsuffix2=ов":2.6,"CITY\tsuffix2=од":3.0,"CITY\tsuffix2=ое":-0.1,"CITY\tsuffix2=ой":-1.0,"CITY\tsuffix2=ок":-1.7,"CITY\tsuffix2=ом":-1.8,"CITY\tsuffix2=п":-1.0,"CITY\tsuffix2=пр":-1.0,"CITY\tsuffix2=р":-1.0,"CITY\tsuffix2=ра":-0.6,"CITY\tsuffix2=рг":3.8,"CITY\tsuffix2=ры":-1.0,"CITY\tsuffix2=с":-0.9,"CITY\tsuffix2=ск":3.9,"CITY\tsuffix2=ти":2.0,"CITY\tsuffix2=ул":-1.0,"CITY\tsuffix2=ха":2.0,"CITY\tsuffix2=ш":-1.0,"CITY\tsuffix2=щи":1.0,"CITY\tsuffix2=ый":-2.0,"CITY\tsuffix3=а":-1.0,"CITY\tsuffix3=б":-1.0,"CITY\tsuffix3=г":-1.0,"CITY\tsuffix3=д":-1.0,"CITY\tsuffix3=ект":-1.0,"CITY\tsuffix3=ина":-0.9,"CITY\tsuffix3=ино":0.9,"CITY\tsuffix3=ира":-0.6,"CITY\tsuffix3=иха":2.0,"CITY\tsuffix3=ищи":2.0,"CITY\tsuffix3=кая":-2.5,"CITY\tsuffix3=ква":4.1,"CITY\tsuffix3=кий":-2.9,"CITY\tsuffix3=кое":-3.7,"CITY\tsuffix3=ком":-1.8,"CITY\tsuffix3=кры":-1.0,"CITY\tsuffix3=лок":-1.7,"CITY\tsuffix3=ний":3.5,"CITY\tsuffix3=нка":-1.0,"CITY\tsuffix3=нкт":2.9,"CITY\tsuffix3=ное":3.6,"CITY\tsuffix3=нск":1.0,"CITY\tsuffix3=ный":-2.0,"CITY\tsuffix3=ова":-2.8,"CITY\tsuffix3=ово":0.3,"CITY\tsuffix3=ого":-1.0,"CITY\tsuffix3=оля":-0.9,"CITY\tsuffix3=ощи":-1.0,"CITY\tsuffix3=п":-1.0,"CITY\tsuffix3=пр":-1.0,"CITY\tsuffix3=р":-1.0,"CITY\tsuffix3=рия":-1.0,"CITY\tsuffix3=ров":2.8,"CITY\tsuffix3=род":3.0,"CITY\tsuffix3=рск":1.0,"CITY\tsuffix3=с":-0.9,"CITY\tsuffix3=сов":-0.9,"CITY\tsuffix3=тов":0.7,"CITY\tsuffix3=тти":2.0,"CITY\tsuffix3=уга":2.8,"CITY\tsuffix3=ул":-1.0,"CITY\tsuffix3=ург":3.8,"CITY\tsuffix3=ш":-1.0,"CITY\tsuffix3=шой":-1.0,"CITY\tsuffix3=ьск":1.9,"CITY\tword=а":-1.0,"CITY\tword=б":-1.0,"CITY\tword=балашиха":2.0,"CITY\tword=большой":-1.0,"CITY\tword=брюсов":-0.9,"CITY\tword=вавилова":-1.0,"CITY\tword=видное":3.6,"CITY\tword=воронина":-0.9,"CITY\tword=г":-1.0,"CITY\tword=д":-1.0,"CITY\tword=дмитрия":-1.0,"CITY\tword=домодедово":1.8,"CITY\tword=ермолино":2.5,"CITY\tword=зеленый":-1.0,"CITY\tword=искры":-1.0,"CITY\tword=калуга":2.8,"CITY\tword=каширское":-0.9,"CITY\tword=киров":2.8,"CITY\tword=кирова":-1.8,"CITY\tword=ком":-1.8,"CITY\tword=кутузовский":-1.0,"CITY\tword=ленинский":-1.0,"CITY\tword=мира":-0.6,"CITY\tword=можайское":-0.8,"CITY\tword=москва":4.1,"CITY\tword=московская":-0.5,"CITY\tword=мытищи":2.0,"CITY\tword=народного":-1.0,"CITY\tword=нахабино":-1.6,"CITY\tword=нижний":3.5,"CITY\tword=новгород":3.0,"CITY\tword=ново":-1.5,"CITY\tword=новоивановское":-1.0,"CITY\tword=очаковское":-1.0,"CITY\tword=п":-1.0,"CITY\tword=переулок":-1.7,"CITY\tword=петербург":3.8,"CITY\tword=подольск":1.9,"CITY\tword=поля":-0.9,"CITY\tword=пр":-1.0,"CITY\tword=пречистенка":-1.0,"CITY\tword=проспект":-1.0,"CITY\tword=р":-1.0,"CITY\tword=реутов":1.6,"CITY\tword=рощи":-1.0,"CITY\tword=с":-0.9,"CITY\tword=садовническая":-1.0,"CITY\tword=санкт":2.9,"CITY\tword=смоленск":1.0,"CITY\tword=сокольнический":-0.9,"CITY\tword=тольятти":2.0,"CITY\tword=ул":-1.0,"CITY\tword=ш":-1.0,"CITY\tword=электрогорск":1.0,"CITY\tword=электродный":-1.0,"CITY\tword=энтузиастов":-0.9,"CITY\tword=ярославская":-1.0,"DISTRICT\tbias":-0.8,"DISTRICT\tbos":-0.6,"DISTRICT\teos":-1.0,"DISTRICT\tkind=word":-0.8,"DISTRICT\tnext=большой":-1.0,"DISTRICT\tnext=видное":1.0,"DISTRICT\tnext=ермолино":0.9,"DISTRICT\tnext=колпакова":-1.0,"DISTRICT\tnext=комсомола":-1.0,"DISTRICT\tnext=нахабино":1.0,"DISTRICT\tnext=новгород":-0.9,"DISTRICT\tnext=новоивановское":1.6,"DISTRICT\tnext=пр":-0.6,"DISTRICT\tnext=р":1.0,"DISTRICT\tnext=северный":-0.8,"DISTRICT\tnext_kind=word":0.2,"DISTRICT\tposition=first":-0.6,"DISTRICT\tposition=last":-1.0,"DISTRICT\tposition=middle":0.8,"DISTRICT\tprefix1=б":1.9,"DISTRICT\tprefix1=д":-0.8,"DISTRICT\tprefix1=к":1.0,"DISTRICT\tprefix1=л":-0.6,"DISTRICT\tprefix1=м":-1.0,"DISTRICT\tprefix1=н":-0.9,"DISTRICT\tprefix1=о":1.6,"DISTRICT\tprefix1=п":-1.0,"DISTRICT\tprefix1=с":-1.0,"DISTRICT\tprefix2=бо":1.9,"DISTRICT\tprefix2=до":-0.8,"DISTRICT\tprefix2=кр":1.0,"DISTRICT\tprefix2=ле":-0.6,"DISTRICT\tprefix2=мы":-1.0,"DISTRICT\tprefix2=ни":-0.9,"DISTRICT\tprefix2=од":1.6,"DISTRICT\tprefix2=пе":-1.0,"DISTRICT\tprefix2=са":-1.0,"DISTRICT\tprev=большой":-1.0,"DISTRICT\tprev=калужская":0.9,"DISTRICT\tprev=московская":1.8,"DISTRICT\tprev=нижегородская":-0.9,"DISTRICT\tprev=санкт":-1.0,"DISTRICT\tprev_kind=word":-0.2,"DISTRICT\tsuffix1=г":-1.0,"DISTRICT\tsuffix1=и":-1.0,"DISTRICT\tsuffix1=й":3.0,"DISTRICT\tsuffix1=о":-1.8,"DISTRICT\tsuffix2=во":-0.8,"DISTRICT\tsuffix2=го":-1.0,"DISTRICT\tsuffix2=ий":3.0,"DISTRICT\tsuffix2=рг":-1.0,"DISTRICT\tsuffix2=щи":-1.0,"DISTRICT\tsuffix3=ищи":-1.0,"DISTRICT\tsuffix3=кий":3.9,"DISTRICT\tsuffix3=ний":-0.9,"DISTRICT\tsuffix3=ово":-0.8,"DISTRICT\tsuffix3=ого":-1.0,"DISTRICT\tsuffix3=ург":-1.0,"DISTRICT\tword=боровский":1.9,"DISTRICT\tword=домодедово":-0.8,"DISTRICT\tword=красногорский":1.0,"DISTRICT\tword=ленинградский":-0.6,"DISTRICT\tword=ленинский":1.0,"DISTRICT\tword=ленинского":-1.0,"DISTRICT\tword=мытищи":-1.0,"DISTRICT\tword=нижний":-0.9,"DISTRICT\tword=одинцовский":1.6,"DISTRICT\tword=петербург":-1.0,"DISTRICT\tword=сампсониевский":-1.0,"O\tbias":1.6,"O\tbos":0.1,"O\teos":0.9,"O\tkind=word":1.6,"O\tnext=б":1.4,"O\tnext=балашиха":-0.2,"O\tnext=г":3.0,"O\tnext=гагарина":-0.9,"O\tnext=гоголя":-1.0,"O\tnext=домодедово":-0.5,"O\tnext=й":-1.0,"O\tnext=каширское":2.0,"O\tnext=киров":-0.7,"O\tnext=комсомола":-1.0,"O\tnext=ленинского":-1.0,"O\tnext=марьиной":1.0,"O\tnext=москва":4.3,"O\tnext=мытищи":-0.6,"O\tnext=нижний":-1.0,"O\tnext=новгород":-1.0,"O\tnext=он":1.0,"O\tnext=переулок":1.0,"O\tnext=петербург":-0.9,"O\tnext=подольск":-0.3,"O\tnext=пр":0.9,"O\tnext=правды":-0.9,"O\tnext=реутов":-0.4,"O\tnext=рощи":-1.0,"O\tnext=с":-2.4,"O\tnext=т":1.0,"O\tnext=электрогорск":-0.1,"O\tnext=этаж":1.0,"O\tnext=ямского":-1.0,"O\tnext_kind=word":0.7,"O\tposition=first":0.1,"O\tposition=last":1.1,"O\tposition=middle":0.4,"O\tprefix1=а":2.0,"O\tprefix1=б":0.2,"O\tprefix1=в":-1.7,"O\tprefix1=г":1.1,"O\tprefix1=д":1.0,"O\tprefix1=е":-0.9,"O\tprefix1=з":-0.8,"O\tprefix1=и":-1.0,"O\tprefix1=й":-1.0,"O\tprefix1=к":-1.1,"O\tprefix1=л":-1.0,"O\tprefix1=м":-0.6,"O\tprefix1=н":-4.0,"O\tprefix1=о":-2.0,"O\tprefix1=п":3.8,"O\tprefix1=р":0.7,"O\tprefix1=т":-1.9,"O\tprefix1=у":3.0,"O\tprefix1=ш":3.9,"O\tprefix1=э":1.0,"O\tprefix1=я":0.9,"O\tprefix2=а":2.7,"O\tprefix2=ав":-0.7,"O\tprefix2=бр":-1.0,"O\tprefix2=бу":1.2,"O\tprefix2=ва":-1.6,"O\tprefix2=ви":-1.0,"O\tprefix2=вл":0.9,"O\tprefix2=г":3.0,"O\tprefix2=га":-0.9,"O\tprefix2=гр":-1.0,"O\tprefix2=д":1.0,"O\tprefix2=ер":-0.9,"O\tprefix2=зе":-0.8,"O\tprefix2=ис":-1.0,"O\tprefix2=й":-1.0,"O\tprefix2=к":1.0,"O\tprefix2=ка":-2.8,"O\tprefix2=ки":-0.7,"O\tprefix2=ко":1.4,"O\tprefix2=ле":-1.0,"O\tprefix2=ма":-1.0,"O\tprefix2=ми":-1.0,"O\tprefix2=мо":1.4,"O\tprefix2=ни":-2.0,"O\tprefix2=но":-2.0,"O\tprefix2=оз":-1.0,"O\tprefix2=ок":-1.0,"O\tprefix2=он":1.0,"O\tprefix2=ор":-1.0,"O\tprefix2=п":1.9,"O\tprefix2=пе":0.7,"O\tprefix2=пл":1.0,"O\tprefix2=по":-1.0,"O\tprefix2=пр":1.2,"O\tprefix2=р":1.7,"O\tprefix2=ро":-1.0,"O\tprefix2=с":1.9,"O\tprefix2=са":-0.9,"O\tprefix2=см":-1.0,"O\tprefix2=т":1.0,"O\tprefix2=та":-0.9,"O\tprefix2=то":-2.0,"O\tprefix2=ул":3.0,"O\tprefix2=ш":1.9,"O\tprefix2=шо":2.0,"O\tprefix2=эн":-1.0,"O\tprefix2=эт":2.0,"O\tprefix2=я":0.9,"O\tprev=а":2.7,"O\tprev=боровский":0.1,"O\tprev=бульвар":0.9,"O\tprev=г":2.3,"O\tprev=ермолино":-0.9,"O\tprev=зеленый":1.0,"O\tprev=земляной":-1.0,"O\tprev=калуга":-0.8,"O\tprev=каширское":1.9,"O\tprev=ком":2.4,"O\tprev=ленинского":-1.0,"O\tprev=марьиной":-1.0,"O\tprev=москва":6.5,"O\tprev=нижегородская":-1.0,"O\tprev=нижний":-1.0,"O\tprev=новоивановское":-1.0,"O\tprev=ордынка":-2.4,"O\tprev=песчаный":-0.9,"O\tprev=поля":-1.8,"O\tprev=проспект":1.0,"O\tprev=р":1.0,"O\tprev=рощи":-1.0,"O\tprev=санкт":-2.0,"O\tprev=сокольнический":-0.6,"O\tprev=ш":-0.9,"O\tprev=ямского":-1.0,"O\tprev_kind=word":1.5,"O\tsuffix1=а":-3.3,"O\tsuffix1=в":-2.0,"O\tsuffix1=г":1.0,"O\tsuffix1=д":1.9,"O\tsuffix1=е":-0.9,"O\tsuffix1=ж":2.0,"O\tsuffix1=и":-2.0,"O\tsuffix1=й":-5.8,"O\tsuffix1=к":1.8,"O\tsuffix1=л":1.4,"O\tsuffix1=м":2.4,"O\tsuffix1=н":1.0,"O\tsuffix1=о":-1.9,"O\tsuffix1=п":1.9,"O\tsuffix1=р":4.0,"O\tsuffix1=с":1.9,"O\tsuffix1=т":1.1,"O\tsuffix1=у":-1.0,"O\tsuffix1=ш":1.9,"O\tsuffix1=щ":0.9,"O\tsuffix1=ы":-2.9,"O\tsuffix1=ь":1.0,"O\tsuffix1=я":-2.8,"O\tsuffix2=а":2.7,"O\tsuffix2=ад":0.9,"O\tsuffix2=аж":2.0,"O\tsuffix2=ал":-1.6,"O\tsuffix2=ар":1.2,"O\tsuffix2=ау":-1.0,"O\tsuffix2=ая":-2.7,"O\tsuffix2=ва":-0.2,"O\tsuffix2=г":3.0,"O\tsuffix2=го":-1.0,"O\tsuffix2=д":1.0,"O\tsuffix2=ды":-1.9,"O\tsuffix2=дь":1.0,"O\tsuffix2=ер":-0.9,"O\tsuffix2=ещ":0.9,"O\tsuffix2=зд":1.0,"O\tsuffix2=ий":-3.0,"O\tsuffix2=й":-1.0,"O\tsuffix2=к":1.0,"O\tsuffix2=ка":-1.9,"O\tsuffix2=кт":0.1,"O\tsuffix2=ла":-1.0,"O\tsuffix2=ля":-1.0,"O\tsuffix2=на":-1.9,"O\tsuffix2=но":-0.9,"O\tsuffix2=ов":-2.0,"O\tsuffix2=од":-1.0,"O\tsuffix2=ое":-2.9,"O\tsuffix2=ой":-1.0,"O\tsuffix2=ок":2.7,"O\tsuffix2=ом":2.4,"O\tsuffix2=он":1.0,"O\tsuffix2=п":1.9,"O\tsuffix2=пр":2.0,"O\tsuffix2=р":1.7,"O\tsuffix2=ра":-1.0,"O\tsuffix2=рг":-2.0,"O\tsuffix2=ры":-1.0,"O\tsuffix2=с":1.9,"O\tsuffix2=се":2.0,"O\tsuffix2=ск":-1.9,"O\tsuffix2=т":1.0,"O\tsuffix2=ти":-1.0,"O\tsuffix2=ул":3.0,"O\tsuffix2=ш":1.9,"O\tsuffix2=щи":-1.0,"O\tsuffix2=ый":-0.8,"O\tsuffix2=я":0.9,"O\tsuffix3=а":2.7,"O\tsuffix3=адь":1.0,"O\tsuffix3=вал":-1.6,"O\tsuffix3=вар":1.2,"O\tsuffix3=вды":-1.9,"O\tsuffix3=г":3.0,"O\tsuffix3=д":1.0,"O\tsuffix3=езд":1.0,"O\tsuffix3=ект":1.0,"O\tsuffix3=ина":-1.9,"O\tsuffix3=ино":-0.9,"O\tsuffix3=ира":-1.0,"O\tsuffix3=й":-1.0,"O\tsuffix3=к":1.0,"O\tsuffix3=кая":-2.0,"O\tsuffix3=ква":-0.2,"O\tsuffix3=кий":-2.0,"O\tsuffix3=кое":-1.9,"O\tsuffix3=ком":2.4,"O\tsuffix3=кры":-1.0,"O\tsuffix3=лад":0.9,"O\tsuffix3=лок":2.7,"O\tsuffix3=мау":-1.0,"O\tsuffix3=мещ":0.9,"O\tsuffix3=ная":-0.7,"O\tsuffix3=ний":-1.0,"O\tsuffix3=нка":-1.9,"O\tsuffix3=нкт":-0.9,"O\tsuffix3=ное":-1.0,"O\tsuffix3=ной":-1.0,"O\tsuffix3=нск":-1.0,"O\tsuffix3=ный":-0.8,"O\tsuffix3=ого":-1.0,"O\tsuffix3=ола":-1.0,"O\tsuffix3=оля":-1.0,"O\tsuffix3=он":1.0,"O\tsuffix3=ощи":-1.0,"O\tsuffix3=п":1.9,"O\tsuffix3=пр":2.0,"O\tsuffix3=р":1.7,"O\tsuffix3=род":-1.0,"O\tsuffix3=с":1.9,"O\tsuffix3=сов":-1.0,"O\tsuffix3=ссе":2.0,"O\tsuffix3=т":1.0,"O\tsuffix3=таж":2.0,"O\tsuffix3=тов":-1.0,"O\tsuffix3=тти":-1.0,"O\tsuffix3=ул":3.0,"O\tsuffix3=ург":-2.0,"O\tsuffix3=ш":1.9,"O\tsuffix3=ьер":-0.9,"O\tsuffix3=ьск":-0.9,"O\tsuffix3=я":0.9,"O\tword=а":2.7,"O\tword=авиамоторная":-0.7,"O\tword=брюсов":-1.0,"O\tword=бульвар":1.2,"O\tword=вал":-1.6,"O\tword=видное":-1.0,"O\tword=влад":0.9,"O\tword=г":3.0,"O\tword=гагарина":-0.9,"O\tword=гримау":-1.0,"O\tword=д":1.0,"O\tword=ермолино":-0.9,"O\tword=зеленый":-0.8,"O\tword=искры":-1.0,"O\tword=й":-1.0,"O\tword=к":1.0,"O\tword=калинина":-1.0,"O\tword=карьер":-0.9,"O\tword=каширское":-0.9,"O\tword=кировская":-0.7,"O\tword=ком":2.4,"O\tword=комсомола":-1.0,"O\tword=ленинского":-1.0,"O\tword=марьиной":-1.0,"O\tword=мира":-1.0,"O\tword=можайское":-1.0,"O\tword=москва":-0.2,"O\tword=московская":2.6,"O\tword=нижегородская":-1.0,"O\tword=нижний":-1.0,"O\tword=новгород":-1.0,"O\tword=новомарьинская":-1.0,"O\tword=озерковская":-1.0,"O\tword=октябрьский":-1.0,"O\tword=он":1.0,"O\tword=ордынка":-1.0,"O\tword=п":1.9,"O\tword=переулок":2.7,"O\tword=петербург":-2.0,"O\tword=площадь":1.0,"O\tword=подольск":-0.9,"O\tword=поля":-1.0,"O\tword=помещ":0.9,"O\tword=пр":2.0,"O\tword=правды":-1.9,"O\tword=пречистенка":-0.9,"O\tword=проезд":1.0,"O\tword=проспект":1.0,"O\tword=р":1.7,"O\tword=рощи":-1.0,"O\tword=с":1.9,"O\tword=санкт":-0.9,"O\tword=смоленск":-1.0,"O\tword=т":1.0,"O\tword=тамбовская":-0.9,"O\tword=товарищеский":-1.0,"O\tword=тольятти":-1.0,"O\tword=ул":3.0,"O\tword=ш":1.9,"O\tword=шоссе":2.0,"O\tword=энтузиастов":-1.0,"O\tword=этаж":2.0,"O\tword=я":0.9,"REGION\tbias":-1.1,"REGION\tbos":-0.5,"REGION\teos":-3.5,"REGION\tkind=word":-1.1,"REGION\tnext=балашиха":0.2,"REGION\tnext=боровский":0.9,"REGION\tnext=домодедово":0.5,"REGION\tnext=калуга":1.0,"REGION\tnext=киров":1.6,"REGION\tnext=красногорский":1.0,"REGION\tnext=ленинский":1.3,"REGION\tnext=можайское":-1.0,"REGION\tnext=москва":-5.5,"REGION\tnext=мытищи":1.6,"REGION\tnext=нижний":1.6,"REGION\tnext=новгород":-0.6,"REGION\tnext=одинцовский":1.6,"REGION\tnext=петербург":-1.0,"REGION\tnext=подольск":0.3,"REGION\tnext=реутов":0.4,"REGION\tnext=рощи":-1.0,"REGION\tnext=сенная":-1.3,"REGION\tnext=смоленск":0.6,"REGION\tnext=тольятти":0.7,"REGION\tnext=шоссе":-0.6,"REGION\tnext=электрогорск":0.1,"REGION\tnext_kind=word":2.4,"REGION\tposition=first":-0.5,"REGION\tposition=middle":-0.6,"REGION\tprefix1=в":-0.6,"REGION\tprefix1=д":-1.0,"REGION\tprefix1=к":1.9,"REGION\tprefix1=м":-1.4,"REGION\tprefix1=н":1.0,"REGION\tprefix1=с":-1.0,"REGION\tprefix2=вя":-0.6,"REGION\tprefix2=де":-1.0,"REGION\tprefix2=ка":1.3,"REGION\tprefix2=ки":0.6,"REGION\tprefix2=ма":-1.0,"REGION\tprefix2=мо":-0.4,"REGION\tprefix2=ни":1.0,"REGION\tprefix2=са":-0.3,"REGION\tprefix2=см":-0.7,"REGION\tprev=ул":-0.6,"REGION\tprev_kind=word":-0.6,"REGION\tsuffix1=а":-1.0,"REGION\tsuffix1=в":-1.0,"REGION\tsuffix1=е":-0.6,"REGION\tsuffix1=й":-1.6,"REGION\tsuffix1=т":-1.0,"REGION\tsuffix1=я":4.1,"REGION\tsuffix2=ая":4.1,"REGION\tsuffix2=ва":-1.0,"REGION\tsuffix2=ий":-0.6,"REGION\tsuffix2=кт":-1.0,"REGION\tsuffix2=ов":-1.0,"REGION\tsuffix2=ое":-0.6,"REGION\tsuffix2=ой":-1.0,"REGION\tsuffix3=кая":4.1,"REGION\tsuffix3=ква":-1.0,"REGION\tsuffix3=кое":-0.6,"REGION\tsuffix3=ний":-0.6,"REGION\tsuffix3=нкт":-1.0,"REGION\tsuffix3=ной":-1.0,"REGION\tsuffix3=ров":-1.0,"REGION\tword=вятская":-0.6,"REGION\tword=дербеневская":-1.0,"REGION\tword=калужская":1.9,"REGION\tword=каширское":-0.6,"REGION\tword=киров":-1.0,"REGION\tword=кировская":1.6,"REGION\tword=марьиной":-1.0,"REGION\tword=москва":-1.0,"REGION\tword=московская":0.6,"REGION\tword=нижегородская":1.6,"REGION\tword=нижний":-0.6,"REGION\tword=самарская":0.7,"REGION\tword=санкт":-1.0,"REGION\tword=смоленская":-0.7,"SETTLEMENT\tbias":-1.2,"SETTLEMENT\tbos":-0.4,"SETTLEMENT\teos":-0.3,"SETTLEMENT\tkind=word":-1.2,"SETTLEMENT\tnext=вокзальная":2.0,"SETTLEMENT\tnext=гагарина":-1.0,"SETTLEMENT\tnext=калинина":2.6,"SETTLEMENT\tnext=каширское":1.9,"SETTLEMENT\tnext=кирова":-1.0,"SETTLEMENT\tnext=ленинградская":-1.9,"SETTLEMENT\tnext=нижний":-0.6,"SETTLEMENT\tnext=пр":-0.9,"SETTLEMENT\tnext=сампсониевский":-1.0,"SETTLEMENT\tnext=ш":-1.0,"SETTLEMENT\tnext_kind=word":-0.9,"SETTLEMENT\tposition=first":-0.4,"SETTLEMENT\tposition=last":-1.4,"SETTLEMENT\tposition=middle":0.6,"SETTLEMENT\tprefix1=б":-1.0,"SETTLEMENT\tprefix1=в":-0.7,"SETTLEMENT\tprefix1=е":-1.0,"SETTLEMENT\tprefix1=к":-2.0,"SETTLEMENT\tprefix1=м":-1.0,"SETTLEMENT\tprefix1=н":3.1,"SETTLEMENT\tprefix1=р":-0.3,"SETTLEMENT\tprefix1=с":2.7,"SETTLEMENT\tprefix1=э":-1.0,"SETTLEMENT\tprefix2=бо":-1.0,"SETTLEMENT\tprefix2=ви":-0.7,"SETTLEMENT\tprefix2=ер":-1.0,"SETTLEMENT\tprefix2=ка":-1.0,"SETTLEMENT\tprefix2=ко":-1.0,"SETTLEMENT\tprefix2=мо":-1.0,"SETTLEMENT\tprefix2=на":2.1,"SETTLEMENT\tprefix2=ни":-0.6,"SETTLEMENT\tprefix2=но":1.6,"SETTLEMENT\tprefix2=ре":-0.3,"SETTLEMENT\tprefix2=се":2.7,"SETTLEMENT\tprefix2=эт":-1.0,"SETTLEMENT\tprev=балашиха":-0.4,"SETTLEMENT\tprev=домодедово":0.9,"SETTLEMENT\tprev=красногорский":1.0,"SETTLEMENT\tprev=москва":-1.0,"SETTLEMENT\tprev=нижний":-1.0,"SETTLEMENT\tprev=одинцовский":1.6,"SETTLEMENT\tprev=петербург":-1.0,"SETTLEMENT\tprev=смоленск":-0.9,"SETTLEMENT\tprev_kind=word":-0.8,"SETTLEMENT\tsuffix1=а":-1.0,"SETTLEMENT\tsuffix1=д":-1.0,"SETTLEMENT\tsuffix1=е":1.8,"SETTLEMENT\tsuffix1=ж":-1.0,"SETTLEMENT\tsuffix1=о":0.6,"SETTLEMENT\tsuffix1=я":-0.6,"SETTLEMENT\tsuffix2=аж":-1.0,"SETTLEMENT\tsuffix2=ая":-0.6,"SETTLEMENT\tsuffix2=ва":-1.0,"SETTLEMENT\tsuffix2=во":-1.9,"SETTLEMENT\tsuffix2=но":2.5,"SETTLEMENT\tsuffix2=од":-1.0,"SETTLEMENT\tsuffix2=ое":1.8,"SETTLEMENT\tsuffix2=ой":-1.0,"SETTLEMENT\tsuffix2=ый":1.0,"SETTLEMENT\tsuffix3=ино":2.5,"SETTLEMENT\tsuffix3=кая":-0.6,"SETTLEMENT\tsuffix3=кое":2.5,"SETTLEMENT\tsuffix3=ное":-0.7,"SETTLEMENT\tsuffix3=ный":1.0,"SETTLEMENT\tsuffix3=ова":-1.0,"SETTLEMENT\tsuffix3=ово":-1.9,"SETTLEMENT\tsuffix3=род":-1.0,"SETTLEMENT\tsuffix3=таж":-1.0,"SETTLEMENT\tsuffix3=шой":-1.0,"SETTLEMENT\tword=большой":-1.0,"SETTLEMENT\tword=видное":-0.7,"SETTLEMENT\tword=ермолино":-1.0,"SETTLEMENT\tword=каширское":-1.0,"SETTLEMENT\tword=колпакова":-1.0,"SETTLEMENT\tword=можайское":-1.0,"SETTLEMENT\tword=научный":-1.4,"SETTLEMENT\tword=нахабино":3.5,"SETTLEMENT\tword=нижегородская":-0.6,"SETTLEMENT\tword=новгород":-1.0,"SETTLEMENT\tword=ново":-1.9,"SETTLEMENT\tword=новоивановское":4.5,"SETTLEMENT\tword=революционный":-0.3,"SETTLEMENT\tword=северный":2.7,"SETTLEMENT\tword=этаж":-1.0,"STREET\tbias":0.8,"STREET\tbos":0.3,"STREET\teos":3.0,"STREET\tkind=word":0.8,"STREET\tnext=б":0.4,"STREET\tnext=боровский":-0.9,"STREET\tnext=буденного":-1.0,"STREET\tnext=вал":0.9,"STREET\tnext=вокзальная":-1.0,"STREET\tnext=володарского":-0.9,"STREET\tnext=воронина":-1.0,"STREET\tnext=донского":1.0,"STREET\tnext=ермолино":-0.9,"STREET\tnext=й":2.0,"STREET\tnext=калинина":-1.6,"STREET\tnext=калуга":-1.0,"STREET\tnext=каширское":-1.9,"STREET\tnext=киров":-0.9,"STREET\tnext=колпакова":-1.0,"STREET\tnext=комсомола":2.0,"STREET\tnext=красногорский":-1.0,"STREET\tnext=ленинградская":3.4,"STREET\tnext=ленинский":-0.8,"STREET\tnext=ленинского":-1.0,"STREET\tnext=марьиной":-1.0,"STREET\tnext=мытищи":-1.0,"STREET\tnext=нахабино":-1.0,"STREET\tnext=новгород":-1.0,"STREET\tnext=новоивановское":-1.6,"STREET\tnext=одинцовский":-1.6,"STREET\tnext=октябрьский":-1.0,"STREET\tnext=ополчения":1.0,"STREET\tnext=петербург":-1.0,"STREET\tnext=пр":3.4,"STREET\tnext=р":-1.0,"STREET\tnext=рощи":2.0,"STREET\tnext=с":2.4,"STREET\tnext=сампсониевский":2.0,"STREET\tnext=северный":-2.0,"STREET\tnext=сенная":1.3,"STREET\tnext=смоленск":-0.6,"STREET\tnext=тольятти":-0.7,"STREET\tnext=ш":1.9,"STREET\tnext=шоссе":0.6,"STREET\tnext=я":1.9,"STREET\tnext_kind=word":-2.2,"STREET\tposition=first":0.3,"STREET\tposition=last":2.5,"STREET\tposition=middle":-2.0,"STREET\tprefix1=а":-1.0,"STREET\tprefix1=б":-0.2,"STREET\tprefix1=в":1.3,"STREET\tprefix1=г":-0.1,"STREET\tprefix1=д":1.0,"STREET\tprefix1=е":-0.6,"STREET\tprefix1=з":1.8,"STREET\tprefix1=и":2.0,"STREET\tprefix1=й":1.0,"STREET\tprefix1=к":0.1,"STREET\tprefix1=л":2.6,"STREET\tprefix1=м":-0.2,"STREET\tprefix1=н":-0.6,"STREET\tprefix1=о":1.4,"STREET\tprefix1=п":-1.9,"STREET\tprefix1=с":-1.8,"STREET\tprefix1=т":-0.1,"STREET\tprefix1=у":-2.0,"STREET\tprefix1=ш":-2.9,"STREET\tprefix1=э":0.9,"STREET\tprefix1=я":0.1,"STREET\tprefix2=а":-1.7,"STREET\tprefix2=ав":0.7,"STREET\tprefix2=б":1.0,"STREET\tprefix2=ба":-2.0,"STREET\tprefix2=бо":0.1,"STREET\tprefix2=бр":1.9,"STREET\tprefix2=бу":-1.2,"STREET\tprefix2=ва":2.6,"STREET\tprefix2=ви":-1.9,"STREET\tprefix2=вл":-0.9,"STREET\tprefix2=во":0.9,"STREET\tprefix2=вя":0.6,"STREET\tprefix2=г":-2.0,"STREET\tprefix2=га":0.9,"STREET\tprefix2=гр":1.0,"STREET\tprefix2=де":1.0,"STREET\tprefix2=дм":1.0,"STREET\tprefix2=до":-1.0,"STREET\tprefix2=ер":-0.6,"STREET\tprefix2=зе":1.8,"STREET\tprefix2=ис":2.0,"STREET\tprefix2=й":1.0,"STREET\tprefix2=к":-1.0,"STREET\tprefix2=ка":0.6,"STREET\tprefix2=ки":-0.9,"STREET\tprefix2=ко":1.4,"STREET\tprefix2=кр":-1.0,"STREET\tprefix2=ку":1.0,"STREET\tprefix2=ле":2.6,"STREET\tprefix2=ма":2.0,"STREET\tprefix2=ми":1.6,"STREET\tprefix2=мо":-2.8,"STREET\tprefix2=мы":-1.0,"STREET\tprefix2=на":0.5,"STREET\tprefix2=ни":-1.0,"STREET\tprefix2=но":-0.1,"STREET\tprefix2=од":-1.6,"STREET\tprefix2=оз":1.0,"STREET\tprefix2=ок":1.0,"STREET\tprefix2=он":-1.0,"STREET\tprefix2=ор":1.0,"STREET\tprefix2=оч":1.0,"STREET\tprefix2=п":-0.9,"STREET\tprefix2=пе":-1.8,"STREET\tprefix2=пл":-1.0,"STREET\tprefix2=пр":1.8,"STREET\tprefix2=р":-0.7,"STREET\tprefix2=ре":-1.3,"STREET\tprefix2=ро":2.0,"STREET\tprefix2=с":-1.0,"STREET\tprefix2=са":0.3,"STREET\tprefix2=се":-2.7,"STREET\tprefix2=см":0.7,"STREET\tprefix2=со":0.9,"STREET\tprefix2=т":-1.0,"STREET\tprefix2=та":0.9,"STREET\tprefix2=ул":-2.0,"STREET\tprefix2=ш":-0.9,"STREET\tprefix2=шо":-2.0,"STREET\tprefix2=эн":1.9,"STREET\tprefix2=эт":-1.0,"STREET\tprefix2=я":-0.9,"STREET\tprefix2=яр":1.0,"STREET\tprev=а":-2.7,"STREET\tprev=балашиха":0.4,"STREET\tprev=большой":1.0,"STREET\tprev=бульвар":-0.9,"STREET\tprev=г":-3.0,"STREET\tprev=домодедово":-0.9,"STREET\tprev=ермолино":0.9,"STREET\tprev=земляной":1.0,"STREET\tprev=калуга":0.8,"STREET\tprev=калужская":-1.9,"STREET\tprev=каширское":-1.9,"STREET\tprev=кировская":-0.9,"STREET\tprev=ком":-2.4,"STREET\tprev=красногорский":-1.0,"STREET\tprev=ленинский":-1.0,"STREET\tprev=ленинского":1.0,"STREET\tprev=марьиной":2.0,"STREET\tprev=москва":5.0,"STREET\tprev=московская":-4.6,"STREET\tprev=нижний":-1.0,"STREET\tprev=новоивановское":1.0,"STREET\tprev=одинцовский":-1.6,"STREET\tprev=ордынка":3.4,"STREET\tprev=песчаный":0.9,"STREET\tprev=петербург":1.0,"STREET\tprev=поля":1.8,"STREET\tprev=проспект":-1.0,"STREET\tprev=р":-1.0,"STREET\tprev=рощи":1.0,"STREET\tprev=санкт":-0.8,"STREET\tprev=смоленск":1.9,"STREET\tprev=сокольнический":0.6,"STREET\tprev=ул":0.6,"STREET\tprev=ш":0.9,"STREET\tprev=ямского":1.9,"STREET\tprev_kind=word":0.5,"STREET\tsuffix1=а":2.7,"STREET\tsuffix1=б":1.0,"STREET\tsuffix1=в":0.4,"STREET\tsuffix1=г":-2.8,"STREET\tsuffix1=д":-2.9,"STREET\tsuffix1=е":-0.2,"STREET\tsuffix1=ж":-1.0,"STREET\tsuffix1=й":6.8,"STREET\tsuffix1=к":-4.0,"STREET\tsuffix1=л":-0.4,"STREET\tsuffix1=м":-0.6,"STREET\tsuffix1=н":-1.0,"STREET\tsuffix1=о":2.9,"STREET\tsuffix1=п":-0.9,"STREET\tsuffix1=р":-2.0,"STREET\tsuffix1=с":-1.0,"STREET\tsuffix1=т":-2.0,"STREET\tsuffix1=у":1.0,"STREET\tsuffix1=ш":-0.9,"STREET\tsuffix1=щ":-0.9,"STREET\tsuffix1=ы":3.9,"STREET\tsuffix1=ь":-1.0,"STREET\tsuffix1=я":3.7,"STREET\tsuffix2=а":-1.7,"STREET\tsuffix2=ад":-0.9,"STREET\tsuffix2=аж":-1.0,"STREET\tsuffix2=ал":1.6,"STREET\tsuffix2=ар":-1.2,"STREET\tsuffix2=ау":1.0,"STREET\tsuffix2=ая":1.7,"STREET\tsuffix2=б":1.0,"STREET\tsuffix2=ва":0.9,"STREET\tsuffix2=во":2.4,"STREET\tsuffix2=г":-2.0,"STREET\tsuffix2=га":-2.8,"STREET\tsuffix2=го":3.0,"STREET\tsuffix2=ды":1.9,"STREET\tsuffix2=дь":-1.0,"STREET\tsuffix2=ер":0.9,"STREET\tsuffix2=ещ":-0.9,"STREET\tsuffix2=зд":-1.0,"STREET\tsuffix2=ия":1.0,"STREET\tsuffix2=й":1.0,"STREET\tsuffix2=к":-1.0,"STREET\tsuffix2=ка":2.9,"STREET\tsuffix2=кт":-1.0,"STREET\tsuffix2=ла":1.0,"STREET\tsuffix2=ля":1.9,"STREET\tsuffix2=на":2.8,"STREET\tsuffix2=но":-2.5,"STREET\tsuffix2=ов":0.4,"STREET\tsuffix2=од":-1.0,"STREET\tsuffix2=ое":1.8,"STREET\tsuffix2=ой":4.0,"STREET\tsuffix2=ок":-1.0,"STREET\tsuffix2=ом":-0.6,"STREET\tsuffix2=он":-1.0,"STREET\tsuffix2=п":-0.9,"STREET\tsuffix2=пр":-1.0,"STREET\tsuffix2=р":-0.7,"STREET\tsuffix2=ра":1.6,"STREET\tsuffix2=рг":-0.8,"STREET\tsuffix2=ры":2.0,"STREET\tsuffix2=с":-1.0,"STREET\tsuffix2=се":-2.0,"STREET\tsuffix2=ск":-2.0,"STREET\tsuffix2=т":-1.0,"STREET\tsuffix2=ти":-1.0,"STREET\tsuffix2=ул":-2.0,"STREET\tsuffix2=ха":-2.0,"STREET\tsuffix2=ш":-0.9,"STREET\tsuffix2=щи":1.0,"STREET\tsuffix2=ый":1.8,"STREET\tsuffix2=я":-0.9,"STREET\tsuffix3=а":-1.7,"STREET\tsuffix3=адь":-1.0,"STREET\tsuffix3=б":1.0,"STREET\tsuffix3=вал":1.6,"STREET\tsuffix3=вар":-1.2,"STREET\tsuffix3=вды":1.9,"STREET\tsuffix3=г":-2.0,"STREET\tsuffix3=езд":-1.0,"STREET\tsuffix3=ина":2.8,"STREET\tsuffix3=ино":-2.5,"STREET\tsuffix3=ира":1.6,"STREET\tsuffix3=иха":-2.0,"STREET\tsuffix3=ищи":-1.0,"STREET\tsuffix3=й":1.0,"STREET\tsuffix3=к":-1.0,"STREET\tsuffix3=кая":1.0,"STREET\tsuffix3=ква":-2.9,"STREET\tsuffix3=кий":1.0,"STREET\tsuffix3=кое":3.7,"STREET\tsuffix3=ком":-0.6,"STREET\tsuffix3=кры":2.0,"STREET\tsuffix3=лад":-0.9,"STREET\tsuffix3=лок":-1.0,"STREET\tsuffix3=мау":1.0,"STREET\tsuffix3=мещ":-0.9,"STREET\tsuffix3=ная":0.7,"STREET\tsuffix3=ний":-1.0,"STREET\tsuffix3=нка":2.9,"STREET\tsuffix3=нкт":-1.0,"STREET\tsuffix3=ное":-1.9,"STREET\tsuffix3=ной":2.0,"STREET\tsuffix3=ный":1.8,"STREET\tsuffix3=ова":3.8,"STREET\tsuffix3=ово":2.4,"STREET\tsuffix3=ого":3.0,"STREET\tsuffix3=ола":1.0,"STREET\tsuffix3=оля":1.9,"STREET\tsuffix3=он":-1.0,"STREET\tsuffix3=ощи":2.0,"STREET\tsuffix3=п":-0.9,"STREET\tsuffix3=пр":-1.0,"STREET\tsuffix3=р":-0.7,"STREET\tsuffix3=рия":1.0,"STREET\tsuffix3=ров":-1.8,"STREET\tsuffix3=род":-1.0,"STREET\tsuffix3=рск":-1.0,"STREET\tsuffix3=с":-1.0,"STREET\tsuffix3=сов":1.9,"STREET\tsuffix3=ссе":-2.0,"STREET\tsuffix3=т":-1.0,"STREET\tsuffix3=таж":-1.0,"STREET\tsuffix3=тов":0.3,"STREET\tsuffix3=тти":-1.0,"STREET\tsuffix3=уга":-2.8,"STREET\tsuffix3=ул":-2.0,"STREET\tsuffix3=ург":-0.8,"STREET\tsuffix3=ш":-0.9,"STREET\tsuffix3=шой":2.0,"STREET\tsuffix3=ьер":0.9,"STREET\tsuffix3=ьск":-1.0,"STREET\tsuffix3=я":-0.9,"STREET\tword=а":-1.7,"STREET\tword=авиамоторная":0.7,"STREET\tword=б":1.0,"STREET\tword=балашиха":-2.0,"STREET\tword=большой":2.0,"STREET\tword=боровский":-1.9,"STREET\tword=брюсов":1.9,"STREET\tword=бульвар":-1.2,"STREET\tword=вавилова":1.0,"STREET\tword=вал":1.6,"STREET\tword=видное":-1.9,"STREET\tword=влад":-0.9,"STREET\tword=воронина":0.9,"STREET\tword=вятская":0.6,"STREET\tword=г":-2.0,"STREET\tword=гагарина":0.9,"STREET\tword=гримау":1.0,"STREET\tword=дербеневская":1.0,"STREET\tword=дмитрия":1.0,"STREET\tword=домодедово":-1.0,"STREET\tword=ермолино":-0.6,"STREET\tword=зеленый":1.8,"STREET\tword=искры":2.0,"STREET\tword=й":1.0,"STREET\tword=к":-1.0,"STREET\tword=калинина":1.0,"STREET\tword=калуга":-2.8,"STREET\tword=калужская":-1.9,"STREET\tword=карьер":0.9,"STREET\tword=каширское":3.4,"STREET\tword=киров":-1.8,"STREET\tword=кирова":1.8,"STREET\tword=кировская":-0.9,"STREET\tword=колпакова":1.0,"STREET\tword=ком":-0.6,"STREET\tword=комсомола":1.0,"STREET\tword=красногорский":-1.0,"STREET\tword=кутузовский":1.0,"STREET\tword=ленинградский":0.6,"STREET\tword=ленинского":2.0,"STREET\tword=марьиной":2.0,"STREET\tword=мира":1.6,"STREET\tword=можайское":2.8,"STREET\tword=москва":-2.9,"STREET\tword=московская":-2.7,"STREET\tword=мытищи":-1.0,"STREET\tword=народного":1.0,"STREET\tword=научный":1.4,"STREET\tword=нахабино":-1.9,"STREET\tword=нижний":-1.0,"STREET\tword=новгород":-1.0,"STREET\tword=ново":3.4,"STREET\tword=новоивановское":-3.5,"STREET\tword=новомарьинская":1.0,"STREET\tword=одинцовский":-1.6,"STREET\tword=озерковская":1.0,"STREET\tword=октябрьский":1.0,"STREET\tword=он":-1.0,"STREET\tword=ордынка":1.0,"STREET\tword=очаковское":1.0,"STREET\tword=п":-0.9,"STREET\tword=переулок":-1.0,"STREET\tword=петербург":-0.8,"STREET\tword=площадь":-1.0,"STREET\tword=подольск":-1.0,"STREET\tword=поля":1.9,"STREET\tword=помещ":-0.9,"STREET\tword=пр":-1.0,"STREET\tword=правды":1.9,"STREET\tword=пречистенка":1.9,"STREET\tword=проезд":-1.0,"STREET\tword=р":-0.7,"STREET\tword=революционный":0.3,"STREET\tword=реутов":-1.6,"STREET\tword=рощи":2.0,"STREET\tword=с":-1.0,"STREET\tword=садовническая":1.0,"STREET\tword=самарская":-0.7,"STREET\tword=сампсониевский":1.0,"STREET\tword=санкт":-1.0,"STREET\tword=северный":-2.7,"STREET\tword=смоленская":0.7,"STREET\tword=сокольнический":0.9,"STREET\tword=т":-1.0,"STREET\tword=тамбовская":0.9,"STREET\tword=товарищеский":1.0,"STREET\tword=тольятти":-1.0,"STREET\tword=ул":-2.0,"STREET\tword=ш":-0.9,"STREET\tword=шоссе":-2.0,"STREET\tword=электрогорск":-1.0,"STREET\tword=электродный":1.0,"STREET\tword=энтузиастов":1.9,"STREET\tword=этаж":-1.0,"STREET\tword=я":-0.9,"STREET\tword=ярославская":1.0},"transitions":{"\tCITY":1.1,"\tDISTRICT":-0.6,"\tO":0.1,"\tREGION":-0.5,"\tSETTLEMENT":-0.4,"\tSTREET":0.3,"CITY\t":0.9,"CITY\tCITY":-2.6,"CITY\tDISTRICT":-0.5,"CITY\tO":0.3,"CITY\tSETTLEMENT":0.4,"CITY\tSTREET":2.2,"DISTRICT\t":-1.0,"DISTRICT\tCITY":1.0,"DISTRICT\tO":-0.6,"DISTRICT\tSETTLEMENT":0.8,"DISTRICT\tSTREET":-1.0,"O\t":0.9,"O\tCITY":1.8,"O\tO":2.6,"O\tREGION":-0.6,"O\tSTREET":-3.1,"REGION\t":-3.5,"REGION\tCITY":4.2,"REGION\tDISTRICT":3.1,"REGION\tO":-0.6,"REGION\tSETTLEMENT":-1.0,"REGION\tSTREET":-3.3,"SETTLEMENT\t":-0.3,"SETTLEMENT\tCITY":-0.6,"SETTLEMENT\tDISTRICT":-1.0,"SETTLEMENT\tO":-1.9,"SETTLEMENT\tSTREET":2.6,"STREET\t":3.0,"STREET\tCITY":-4.2,"STREET\tDISTRICT":-1.8,"STREET\tO":1.7,"STREET\tSETTLEMENT":-1.0,"STREET\tSTREET":3.1}} \ No newline at end of file diff --git a/tests.py b/tests.py deleted file mode 100644 index d3a41a2..0000000 --- a/tests.py +++ /dev/null @@ -1,83 +0,0 @@ -import pandas as pd -from api import get_addr - -dadata_LUT = {'original': 'Исходный адрес', - 'fullname': 'Адрес', - 'index': 'Индекс', - 'postalcode': 'Индекс', - 'country': 'Страна', - 'region_type': 'Тип региона', - 'region': 'Регион', - 'area_type': 'Тип района', - 'area': 'Район', - 'city_type': 'Тип города', - 'city': 'Город', - '65_type': 'Тип н/п', - '65': 'Н/п', - '???': 'Адм. округ', - 'town': 'Н/п', - 'town_type': 'Тип н/п', - 'district_type': 'Тип района', - 'district': 'Район города', - 'street_type': 'Тип улицы', - 'street': 'Улица', - 'house_type': 'Тип дома', - # 'housenum':'Дом', - # 'build_type':'Тип корпуса/строения', - # 'buildnum':'Корпус/строение', - # 'struc_type':'Тип корпуса/строения', - # 'strucnum':'Корпус/строение', - 'flat_type': 'Тип квартиры', - 'flat_num': 'Номер Квартиры', - 'houseid': "Код ФИАС" - } - - -def score(ref, orig_col="Исходный адрес", func=get_addr, - cols_to_score=["Регион", "Город", "Н/п", "Район", "Улица", "Дом", "Корпус/строение"]): - df_1 = func(ref[orig_col]).rename(index=str, columns=dadata_LUT) - if 'Индекс' in cols_to_score: - ref["Индекс"] = ref["Индекс"].fillna('9999999').astype(int).astype(str).replace('9999999', '') - ref, df_1 = ref.fillna(''), df_1.fillna('') - df_1 = df_1.to_dict(orient='records') - N = ref.shape[0] - ref = ref.to_dict(orient='records') - n = 0 - correct = 0 - df = [] - if len(df_1) != len(ref): - return "не совпадают размеры таблицы" - for i, row in enumerate(ref): - for key, value in row.items(): - if key in cols_to_score: # ["Регион", "Индекс", "Район", "Город", "Н/п", "Улица", "Дом", "Корпус/строение"]: - n += 1 - if str(value) == str(df_1[i][key]): - correct += 1 - else: - df.append(df_1[i]) - print("\n{0:03.1f}% correct fields".format(correct / n * 100)) - df = pd.DataFrame(df).drop_duplicates() - print("\n{0:03.1f}% correct lines".format((N - df.shape[0]) / N * 100)) - return df - - -def score_by_id(ref, orig_col="Исходный адрес", func=get_addr): - ref['Тип корпуса/строения'].astype(str) - ref = ref[[orig_col, 'Код ФИАС']][ - (ref['Уровень по ФИАС'] == '8: дом') & (ref['Тип корпуса/строения'].astype(str) != 'nan')] - predicted_df = func(ref[orig_col]) - true_id = ref['Код ФИАС'] - predicted_id = predicted_df['houseid'] - correct = 0 - incorrect = 0 - for true, predicted in zip(true_id, predicted_id): - if true == predicted: - correct += 1 - else: - incorrect += 1 - accuracy = correct / (correct + incorrect) * 100 - print() - print("Accuracy: {0:03.3f}%".format(accuracy)) - - -ref = pd.read_excel('ref/references.xlsx') diff --git a/tests_v2/test_adversarial.py b/tests/test_adversarial.py similarity index 100% rename from tests_v2/test_adversarial.py rename to tests/test_adversarial.py diff --git a/tests_v2/test_api.py b/tests/test_api.py similarity index 100% rename from tests_v2/test_api.py rename to tests/test_api.py diff --git a/tests_v2/test_detection.py b/tests/test_detection.py similarity index 61% rename from tests_v2/test_detection.py rename to tests/test_detection.py index 1729ef1..a0fec88 100644 --- a/tests_v2/test_detection.py +++ b/tests/test_detection.py @@ -1,26 +1,11 @@ from __future__ import annotations -import importlib.util import json -from pathlib import Path import pytest from address_normalizer import DetectedAddress, detect_addresses - -ROOT = Path(__file__).parents[1] - - -def _load_evaluator(): - path = ROOT / "evaluation/evaluate_detection.py" - spec = importlib.util.spec_from_file_location("evaluate_detection", path) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - def test_detects_exact_span_and_keeps_component_offsets_relative(): message = ( "Курьер приедет по адресу: Москва, ул. Тверская, " @@ -86,41 +71,3 @@ def test_conservative_policy_rejects_weak_address_evidence(message): def test_detection_rejects_non_string_input(): with pytest.raises(TypeError, match="text must be a string"): detect_addresses(None) - - -def test_detection_reference_has_detailed_scenario_columns_and_passes(): - evaluator = _load_evaluator() - rows = evaluator.load_rows(ROOT / "evaluation/detection_reference.jsonl") - - assert len(rows) == 30 - assert all( - { - "id", - "message", - "expected", - "scenario_family", - "context_style", - "address_style", - "boundary_style", - "polarity", - "ambiguity", - "notes", - } - <= set(row) - for row in rows - ) - report = evaluator.evaluate(rows) - assert report["exact_span_micro"]["f1"] == 1.0 - assert report["negative_message_specificity"] == 1.0 - assert report["failure_sample"] == [] - - -def test_committed_detection_report_matches_current_result(): - evaluator = _load_evaluator() - rows = evaluator.load_rows(ROOT / "evaluation/detection_reference.jsonl") - actual = evaluator.evaluate(rows) - committed = json.loads( - (ROOT / "evaluation/detection_report.json").read_text(encoding="utf-8") - ) - - assert actual == committed diff --git a/tests_v2/test_model.py b/tests/test_model.py similarity index 89% rename from tests_v2/test_model.py rename to tests/test_model.py index 1e255cd..fae4a3c 100644 --- a/tests_v2/test_model.py +++ b/tests/test_model.py @@ -9,7 +9,7 @@ from address_normalizer import parse from address_normalizer.tagger import CompactSequenceTagger from address_normalizer.tokenizer import tokenize -from training.real_corpus import ( +from tools.model_data import ( build_examples, corpus_summary, load_reference_rows, @@ -38,13 +38,13 @@ def test_bundled_model_was_trained_on_grouped_real_examples(): ) training = payload["training"] assert training["algorithm"] == "epoch-averaged structured perceptron" - assert training["dataset"] == "evaluation/legacy_reference_500.jsonl" + assert training["dataset"] == "benchmarks/legacy_500.jsonl" assert training["examples"] >= 300 def test_real_corpus_has_disjoint_nonempty_splits(): examples = build_examples( - load_reference_rows(ROOT / "evaluation/legacy_reference_500.jsonl") + load_reference_rows(ROOT / "benchmarks/legacy_500.jsonl") ) summary = corpus_summary(examples) assert summary["leaking_groups"] == [] diff --git a/tests_v2/test_runtime_contract.py b/tests/test_runtime_contract.py similarity index 100% rename from tests_v2/test_runtime_contract.py rename to tests/test_runtime_contract.py diff --git a/tests_v2/test_spans.py b/tests/test_spans.py similarity index 100% rename from tests_v2/test_spans.py rename to tests/test_spans.py diff --git a/tests_v2/test_datamos_evaluation.py b/tests_v2/test_datamos_evaluation.py deleted file mode 100644 index 1e64403..0000000 --- a/tests_v2/test_datamos_evaluation.py +++ /dev/null @@ -1,63 +0,0 @@ -from pathlib import Path -import sys - -ROOT = Path(__file__).parents[1] -sys.path.insert(0, str(ROOT / "evaluation")) - -from datamos_data import ( - expected_components, - group_id_and_split, - rejection_reason, -) -from evaluate_datamos import _summary, score - - -def _row() -> dict: - return { - "OnTerritoryOfMoscow": "да", - "ADR_TYPE": "Официальный", - "SOSTAD": "Зарегистрирован в АР", - "STATUS": "Внесён в ГКН", - "SIMPLE_ADDRESS": "Косинская улица, дом 26А", - "ADDRESS": "город Москва, Косинская улица, дом 26А", - "P7": "Косинская улица", - "L1_VALUE": "26А", - "L2_VALUE": "", - "L3_VALUE": "", - "N_FIAS": "235212A3-01E8-4CC3-87D5-59F00C83898A", - } - - -def test_datamos_high_confidence_filter_and_grouping(): - row = _row() - assert rejection_reason(row) is None - assert expected_components(row) == { - "street": "Косинская улица", - "house_num": "26А", - "corpus": None, - "structure": None, - } - group_id, split = group_id_and_split(row) - assert len(group_id) == 32 - assert split in {"train", "validation", "test"} - row["STATUS"] = "Аннулирован в ГКН" - assert rejection_reason(row) == "not_in_gkn" - - -def test_exact_moscow_component_evaluation(): - row = { - "source_row": 0, - "fias_id": "235212a3-01e8-4cc3-87d5-59f00c83898a", - "tier": "house_only", - "raw": "Косинская улица, дом 26А", - "expected": expected_components(_row()), - } - report = score([row]) - assert report["micro"]["f1"] == 1.0 - assert report["exact_address_rate"] == 1.0 - assert report["exact_component_value_micro"] == report["micro"] - assert report["metric_definitions"]["fields"].startswith("per-field") - summary = _summary(report) - assert summary["micro"] == report["exact_component_value_micro"] - assert "metric_definitions" not in summary - assert "exact_component_value_micro" not in summary diff --git a/tests_v2/test_deepparse_evaluation.py b/tests_v2/test_deepparse_evaluation.py deleted file mode 100644 index 02d93d6..0000000 --- a/tests_v2/test_deepparse_evaluation.py +++ /dev/null @@ -1,119 +0,0 @@ -from pathlib import Path -import sys - -ROOT = Path(__file__).parents[1] -sys.path.insert(0, str(ROOT / "evaluation")) - -from deepparse_data import ( - expected_components, - group_id_and_split, - mapped_labels, - normalized_address_id, - quality_tier, -) -from evaluate_deepparse import _score, _summary, gold_spans, predicted_spans -from address_normalizer import parse - - -def test_deepparse_tags_map_to_package_fields(): - tags = ( - "Country", - "Municipality", - "StreetName", - "StreetName", - "StreetNumber", - "Unit", - ) - labels = mapped_labels(tags) - assert labels == ( - "O", - "CITY", - "STREET", - "STREET", - "HOUSE", - "APARTMENT", - ) - assert expected_components( - ("Россия", "Самара", "ул", "Авроры", "7", "12"), - labels, - ) == { - "postal_code": None, - "region": None, - "district": None, - "city": "Самара", - "street": "ул Авроры", - "house_num": "7", - "apartment": "12", - } - assert quality_tier(tags) == "street_house_unit" - - -def test_format_variants_share_a_split_and_exact_text_ids_do_not(): - tokens = ("Самара", "ул", "Авроры", "7", "12") - tags = ( - "Municipality", - "StreetName", - "StreetName", - "StreetNumber", - "Unit", - ) - group_a = group_id_and_split(tokens, tags) - group_b = group_id_and_split(tokens[:-1], tags[:-1]) - assert group_a == group_b - assert normalized_address_id("Самара ул Авроры 7") == ( - normalized_address_id(" самара УЛ авроры 7 ") - ) - assert normalized_address_id("Самара ул Авроры 8") != ( - normalized_address_id("Самара ул Авроры 7") - ) - - -def test_gold_and_predicted_spans_align_on_a_conventional_address(): - row = { - "raw": "Россия Самара ул Авроры 7 12", - "tokens": ["Россия", "Самара", "ул", "Авроры", "7", "12"], - "labels": ["O", "CITY", "STREET", "STREET", "HOUSE", "APARTMENT"], - } - assert [ - (span.label, row["raw"][span.start : span.end]) - for span in gold_spans(row) - ] == [ - ("CITY", "Самара"), - ("STREET", "ул Авроры"), - ("HOUSE", "7"), - ("APARTMENT", "12"), - ] - result = parse(row["raw"]) - assert { - (span.label, row["raw"][span.start : span.end]) - for span in predicted_spans(result) - } >= { - ("CITY", "Самара"), - ("STREET", "ул Авроры"), - ("HOUSE", "7"), - ("APARTMENT", "12"), - } - - row.update( - { - "source_row": 1, - "example_id": "example", - "tier": "street_house_unit", - } - ) - report = _score([row]) - assert report["character_micro"]["f1"] == 1.0 - assert report["token_micro"]["f1"] == 1.0 - assert report["exact_token_sequence_rate"] == 1.0 - assert report["span_overlap_micro"] == report["micro"] - assert report["character_overlap_micro"] == report["character_micro"] - assert report["token_label_micro"] == report["token_micro"] - assert report["metric_definitions"]["fields"].startswith("per-field") - assert report["metric_definitions"]["character_fields"].startswith("per-field") - assert report["metric_definitions"]["token_fields"].startswith("per-field") - summary = _summary(report) - assert summary["micro"] == report["span_overlap_micro"] - assert "metric_definitions" not in summary - assert "span_overlap_micro" not in summary - assert "character_overlap_micro" not in summary - assert "token_label_micro" not in summary diff --git a/tests_v2/test_evaluation.py b/tests_v2/test_evaluation.py deleted file mode 100644 index 71c0690..0000000 --- a/tests_v2/test_evaluation.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import annotations - -import json -from pathlib import Path -import subprocess -import sys - -import pytest - - -ROOT = Path(__file__).parents[1] - - -def test_legacy_reference_is_unique_and_fixed_size(): - rows = [ - json.loads(line) - for line in (ROOT / "evaluation/legacy_reference_500.jsonl") - .read_text(encoding="utf-8") - .splitlines() - if line.strip() - ] - assert len(rows) == 500 - assert len({row["raw"] for row in rows}) == 500 - assert all(row["expected"].get("street") for row in rows) - assert all(row["expected"].get("house_num") for row in rows) - - -def test_legacy_release_gate_passes(): - completed = subprocess.run( - [ - sys.executable, - str(ROOT / "evaluation/evaluate.py"), - "--data", - str(ROOT / "evaluation/legacy_reference_500.jsonl"), - "--gates", - str(ROOT / "evaluation/release_gates.json"), - ], - check=False, - capture_output=True, - text=True, - ) - assert completed.returncode == 0, completed.stdout + completed.stderr - report = json.loads(completed.stdout) - assert report["release_gate_passed"] is True - assert report["exact_component_value_micro"] == report["micro"] - assert "exact component values" in ( - report["metric_definitions"]["exact_component_value_micro"] - ) - committed = json.loads( - (ROOT / "evaluation/legacy_reference_500_report.json").read_text( - encoding="utf-8" - ) - ) - assert committed == report - - -@pytest.mark.parametrize( - ("filename", "aliases"), - [ - ("redmadrobot_report.json", {"span_overlap_micro": "micro"}), - ( - "deepparse_report.json", - { - "span_overlap_micro": "micro", - "character_overlap_micro": "character_micro", - "token_label_micro": "token_micro", - }, - ), - ("datamos_report.json", {"exact_component_value_micro": "micro"}), - ], -) -def test_committed_reports_use_explicit_metric_names(filename, aliases): - report = json.loads((ROOT / "evaluation" / filename).read_text(encoding="utf-8")) - - assert report["metric_definitions"] - for explicit_name, compatibility_name in aliases.items(): - assert report[explicit_name] == report[compatibility_name] diff --git a/tests_v2/test_external_evaluation.py b/tests_v2/test_external_evaluation.py deleted file mode 100644 index fe39b9d..0000000 --- a/tests_v2/test_external_evaluation.py +++ /dev/null @@ -1,117 +0,0 @@ -import json -from pathlib import Path -import sys - -ROOT = Path(__file__).parents[1] -sys.path.insert(0, str(ROOT)) - -from address_normalizer import parse -from evaluation.evaluate_redmadrobot import ( - AddressSnippet, - _gold_spans, - _reconstruct, - _without_failures, - evaluate, -) -from evaluation.evaluate_redmadrobot_detection import ( - Message as DetectionMessage, - Span as DetectionSpan, - evaluate as evaluate_detection, -) - - -def _snippet( - tokens: tuple[str, ...], - tags: tuple[str, ...], -) -> AddressSnippet: - text, offsets = _reconstruct(tokens) - return AddressSnippet( - source_row=0, - text=text, - tokens=tokens, - tags=tags, - offsets=offsets, - ) - - -def test_gold_bio_spans_are_reconstructed_from_tokens(): - snippet = _snippet( - ("ул", ".", "Ополченская", ",", "дом", "5"), - ( - "B-STREET", - "I-STREET", - "I-STREET", - "O", - "B-HOUSE", - "I-HOUSE", - ), - ) - assert [ - (span.label, snippet.text[span.start : span.end]) - for span in _gold_spans(snippet) - ] == [ - ("STREET", "ул . Ополченская"), - ("HOUSE", "дом 5"), - ] - - -def test_external_span_evaluation_accepts_overlapping_component_values(): - snippet = _snippet( - ("ул", ".", "Ополченская", ",", "дом", "5"), - ( - "B-STREET", - "I-STREET", - "I-STREET", - "O", - "B-HOUSE", - "I-HOUSE", - ), - ) - report = evaluate([snippet], parse) - assert report["micro"]["support"] == 2 - assert report["micro"]["tp"] == 2 - assert report["micro"]["f1"] == 1.0 - assert report["span_overlap_micro"] == report["micro"] - assert report["metric_definitions"]["fields"].startswith("per-field") - summary = _without_failures(report) - assert summary["micro"] == report["span_overlap_micro"] - assert "metric_definitions" not in summary - assert "span_overlap_micro" not in summary - - -def test_complete_message_detection_is_scored_without_oracle_cropping(): - text = "Текст до ул. Мира, д. 2 после" - expected_text = "ул. Мира, д. 2" - start = text.index(expected_text) - report = evaluate_detection( - [ - DetectionMessage( - source_row=1, - text=text, - gold=(DetectionSpan(start, start + len(expected_text)),), - ), - DetectionMessage( - source_row=2, - text="Встреча завтра в 18:30", - gold=(), - ), - ] - ) - - assert report["span_overlap_micro"]["f1"] == 1.0 - assert report["exact_span_micro"]["f1"] == 1.0 - assert report["negative_message_specificity"] == 1.0 - assert report["failure_case_count"] == 0 - - -def test_committed_message_detection_report_keeps_all_failure_cases(): - report = json.loads( - ( - ROOT / "evaluation/redmadrobot_detection_report.json" - ).read_text(encoding="utf-8") - ) - - assert report["source"]["revision"] - assert report["metric_definitions"]["span_overlap_micro"] - assert report["failure_case_count"] == len(report["failure_cases"]) - assert report["failure_case_count"] == 107 diff --git a/tests_v2/test_failure_analysis.py b/tests_v2/test_failure_analysis.py deleted file mode 100644 index ba20d74..0000000 --- a/tests_v2/test_failure_analysis.py +++ /dev/null @@ -1,85 +0,0 @@ -from __future__ import annotations - -import csv -import importlib.util -import json -from pathlib import Path - - -ROOT = Path(__file__).parents[1] - - -def _load_analyzer(): - path = ROOT / "evaluation/analyze_failures.py" - spec = importlib.util.spec_from_file_location("analyze_failures", path) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def test_diagnostic_table_covers_every_reference_row_with_narrow_columns(): - with ( - ROOT / "evaluation/legacy_reference_500_diagnostics.csv" - ).open(encoding="utf-8", newline="") as source: - rows = list(csv.DictReader(source)) - - assert len(rows) == 500 - assert len({row["id"] for row in rows}) == 500 - required = { - "exact_address", - "triage_priority", - "diagnosis_status", - "mismatch_fields", - "missing_fields", - "extra_fields", - "wrong_value_fields", - "failure_types", - "primary_likely_cause", - "likely_causes", - "failure_summary", - "warnings", - "unparsed_spans", - "scenario_tags", - "street_marker_style", - "has_compact_punctuation", - "has_compound_number", - "has_unmarked_numeric_sequence", - "expected_street", - "actual_street", - "status_street", - "expected_house_num", - "actual_house_num", - "status_house_num", - } - assert required <= set(rows[0]) - assert sum(row["exact_address"] == "false" for row in rows) == 98 - assert all( - row["diagnosis_status"] == "heuristic_needs_human_review" - for row in rows - if row["exact_address"] == "false" - ) - - -def test_failure_summary_matches_current_diagnostics(): - analyzer = _load_analyzer() - source_rows = analyzer._load_rows( - ROOT / "evaluation/legacy_reference_500.jsonl" - ) - diagnostics = [analyzer.diagnose_row(row) for row in source_rows] - with ( - ROOT / "evaluation/legacy_reference_500_diagnostics.csv" - ).open(encoding="utf-8", newline="") as source: - committed_diagnostics = list(csv.DictReader(source)) - actual = analyzer.summarize(diagnostics) - committed = json.loads( - ( - ROOT / "evaluation/legacy_reference_500_failure_summary.json" - ).read_text(encoding="utf-8") - ) - - assert diagnostics == committed_diagnostics - assert actual == committed - assert actual["rows"] == 500 - assert actual["failed_rows"] == 98 - assert len(actual["representative_failure_sample"]) == 10 diff --git a/evaluation/evaluate.py b/tools/benchmark.py similarity index 86% rename from evaluation/evaluate.py rename to tools/benchmark.py index bfe339e..7309452 100644 --- a/evaluation/evaluate.py +++ b/tools/benchmark.py @@ -9,6 +9,10 @@ import sys from typing import Any, Callable, Iterable + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) + from address_normalizer import parse from address_normalizer.types import ParsedAddress @@ -219,22 +223,51 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument( "--data", type=Path, - default=Path(__file__).with_name("legacy_reference_500.jsonl"), + default=ROOT / "benchmarks/legacy_500.jsonl", + ) + parser.add_argument( + "--check", + action="store_true", + help="fail unless the release regression thresholds pass", ) - parser.add_argument("--gates", type=Path) parser.add_argument("--output", type=Path) args = parser.parse_args(argv) report = score_rows(_load_jsonl(args.data)) - if args.gates: - gates = json.loads(args.gates.read_text(encoding="utf-8")) + if args.check: + gates = { + "minimum_rows": 500, + "minimum_metrics": { + "exact_address_rate": 0.80, + "no_unparsed_rate": 0.75, + "exact_component_value_micro.f1": 0.95, + }, + } report["gates"] = _check_gates(report, gates) report["release_gate_passed"] = all( outcome["passed"] for outcome in report["gates"] ) rendered = json.dumps(report, ensure_ascii=False, indent=2) - print(rendered) + if args.check: + print( + json.dumps( + { + "rows": report["rows"], + "exact_address_rate": report["exact_address_rate"], + "no_unparsed_rate": report["no_unparsed_rate"], + "exact_component_value_micro": report[ + "exact_component_value_micro" + ], + "gates": report["gates"], + "release_gate_passed": report["release_gate_passed"], + }, + ensure_ascii=False, + indent=2, + ) + ) + else: + print(rendered) if args.output: args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(f"{rendered}\n", encoding="utf-8") diff --git a/training/real_corpus.py b/tools/model_data.py similarity index 100% rename from training/real_corpus.py rename to tools/model_data.py diff --git a/training/baselines/synthetic_model.json b/tools/synthetic_model.json similarity index 100% rename from training/baselines/synthetic_model.json rename to tools/synthetic_model.json diff --git a/training/train_compact_tagger.py b/tools/train_model.py similarity index 95% rename from training/train_compact_tagger.py rename to tools/train_model.py index b64807f..3b9653c 100644 --- a/training/train_compact_tagger.py +++ b/tools/train_model.py @@ -23,8 +23,8 @@ START, token_features, ) -from evaluation.evaluate import score_rows -from training.real_corpus import ( +from benchmark import score_rows +from model_data import ( SequenceExample, build_examples, corpus_summary, @@ -222,7 +222,7 @@ def _model_payload( "source-verifiable legacy reference rows with deterministic " "marker-free views" ), - "dataset": "evaluation/legacy_reference_500.jsonl", + "dataset": "benchmarks/legacy_500.jsonl", "dataset_sha256": dataset_sha256, "split": "SHA-256 by canonical address group: 70/15/15", }, @@ -344,8 +344,7 @@ def train( report = { "scope": ( - "group-disjoint real-address model evaluation; source and " - "derived-model provenance are recorded in LICENSING.md" + "group-disjoint real-address model evaluation" ), "dataset": dataset_name, "dataset_sha256": dataset_sha256, @@ -380,7 +379,7 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument( "--data", type=Path, - default=ROOT / "evaluation/legacy_reference_500.jsonl", + default=ROOT / "benchmarks/legacy_500.jsonl", ) parser.add_argument( "--output", @@ -390,12 +389,11 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument( "--report", type=Path, - default=ROOT / "training/model_evaluation.json", ) parser.add_argument( "--baseline-model", type=Path, - default=ROOT / "training/baselines/synthetic_model.json", + default=ROOT / "tools/synthetic_model.json", ) parser.add_argument("--seed", type=int, default=2017) parser.add_argument("--epoch-grid", default="5,10,20,40,80") @@ -419,17 +417,18 @@ def main(argv: list[str] | None = None) -> int: json.dumps(payload, ensure_ascii=False, separators=(",", ":")), encoding="utf-8", ) - args.report.parent.mkdir(parents=True, exist_ok=True) - args.report.write_text( - f"{json.dumps(report, ensure_ascii=False, indent=2)}\n", - encoding="utf-8", - ) + if args.report: + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text( + f"{json.dumps(report, ensure_ascii=False, indent=2)}\n", + encoding="utf-8", + ) print( json.dumps( { "output": str(args.output), "bytes": args.output.stat().st_size, - "report": str(args.report), + "report": str(args.report) if args.report else None, "selected_epochs": report["selected_epochs"], "test_sequence": { name: { diff --git a/training/README.md b/training/README.md deleted file mode 100644 index b0279d8..0000000 --- a/training/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# Training the compact tagger - -The runtime model is a sparse linear-chain sequence tagger with Viterbi -inference. Training uses an epoch-averaged structured perceptron. Training and -inference require only the Python standard library. - -The real-address corpus is derived from the 500 source-verifiable rows in -`evaluation/legacy_reference_500.jsonl`: - -1. the deterministic parser records the exact residual word tokens that reach - the model; -2. source-verifiable address fields are aligned to those token offsets; -3. marker-free views are derived from the same real component names; -4. examples are grouped by canonical administrative/street identity; -5. SHA-256 assigns whole groups to train, validation, or test (70/15/15); -6. epoch count is chosen on validation only; -7. the final candidate is evaluated once on the untouched test groups. - -Regenerate the model and committed evaluation report: - -```bash -python training/train_compact_tagger.py -``` - -Verify the bundled artifact against fixed test gates: - -```bash -python training/evaluate_compact_tagger.py -``` - -The first real model is 37 KB. Compared with the preserved 20 KB synthetic -starter on the group-disjoint sequence test: - -| Metric | Synthetic starter | Real model | -| --- | ---: | ---: | -| Token accuracy | 71.3% | 96.5% | -| Complete sequence accuracy | 61.4% | 94.3% | -| Micro entity F1 | 78.1% | 96.9% | - -On the 21 corresponding end-to-end holdout rows, micro field F1 improves from -87.4% to 91.3%. The test split contains no independently measured -`DISTRICT` or `SETTLEMENT` tokens, so those classes must not be claimed as -validated by this result. See `model_evaluation.json` for the exact split, -tuning runs, class coverage, failures, and end-to-end comparison. - -`training/baselines/synthetic_model.json` preserves the pre-real-data model so -that regeneration remains reproducible and comparisons do not silently change -after the bundled model is replaced. - -## Provenance - -The maintainer authorized redistribution of the historical source workbook, -its committed 500-row derivative, and the compact model under GPL-3.0-only. -The decision, source commit, deterministic generation path, and external-data -boundary are recorded in `LICENSING.md`. - -The external preparation tools now expose 5,681,842 Deepparse training rows and -276,368 historical Moscow-registry training rows in group-disjoint splits. -They are not used by the current 37 KB model. Before training on them, define a -sampling policy so repeated clean formatting does not overwhelm the smaller -noisy-input corpus, keep the committed test groups sealed, and record the -derived-model rights for CC BY 4.0 and the Moscow source terms. - -For a production training release: - -1. independently review at least 300 aligned rows; -2. add substantially more district and settlement examples; -3. preserve canonical-address grouping across every split; -4. reserve a final dataset not used for feature or rule changes; -5. report confidence intervals and field metrics by region and source system; -6. document the right to redistribute examples and the derived model. diff --git a/training/__init__.py b/training/__init__.py deleted file mode 100644 index 2caf800..0000000 --- a/training/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Reproducible training and evaluation helpers.""" diff --git a/training/evaluate_compact_tagger.py b/training/evaluate_compact_tagger.py deleted file mode 100644 index 2d0e81f..0000000 --- a/training/evaluate_compact_tagger.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Evaluate the bundled model on the untouched canonical-group test split.""" - -from __future__ import annotations - -import json -from pathlib import Path -import sys - - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT / "src")) -sys.path.insert(0, str(ROOT)) - -from address_normalizer.tagger import CompactSequenceTagger -from training.real_corpus import ( - build_examples, - corpus_summary, - load_reference_rows, -) -from training.train_compact_tagger import score_sequences - - -MINIMUMS = { - "token_accuracy": 0.95, - "sequence_accuracy": 0.90, - "macro_entity_f1": 0.95, - "micro_entity_f1": 0.95, -} -MAXIMUM_MODEL_BYTES = 250_000 - - -def main() -> int: - data_path = ROOT / "evaluation/legacy_reference_500.jsonl" - model_path = ROOT / "src/address_normalizer/data/model.json" - examples = build_examples(load_reference_rows(data_path)) - test_examples = [example for example in examples if example.split == "test"] - summary = corpus_summary(examples) - metrics = score_sequences( - CompactSequenceTagger.from_package(), - test_examples, - ) - supported_labels = [ - label - for label, values in metrics["labels"].items() - if label != "O" and values["support"] - ] - unsupported_labels = [ - label - for label, values in metrics["labels"].items() - if label != "O" and not values["support"] - ] - gates = [ - { - "metric": name, - "actual": metrics[name], - "minimum": minimum, - "passed": metrics[name] >= minimum, - } - for name, minimum in MINIMUMS.items() - ] - gates.extend( - ( - { - "metric": "model_bytes", - "actual": model_path.stat().st_size, - "maximum": MAXIMUM_MODEL_BYTES, - "passed": model_path.stat().st_size <= MAXIMUM_MODEL_BYTES, - }, - { - "metric": "leaking_groups", - "actual": len(summary["leaking_groups"]), - "maximum": 0, - "passed": not summary["leaking_groups"], - }, - ) - ) - report = { - "scope": ( - "untuned test split grouped by canonical address; source and " - "derived-model provenance are recorded in LICENSING.md" - ), - "test_groups": summary["splits"]["test"]["groups"], - "test_examples": len(test_examples), - "supported_test_labels": supported_labels, - "unsupported_test_labels": unsupported_labels, - "metrics": metrics, - "gates": gates, - "passed": all(gate["passed"] for gate in gates), - } - print(json.dumps(report, ensure_ascii=False, indent=2)) - return 0 if report["passed"] else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/training/model_evaluation.json b/training/model_evaluation.json deleted file mode 100644 index 4610490..0000000 --- a/training/model_evaluation.json +++ /dev/null @@ -1,1316 +0,0 @@ -{ - "scope": "group-disjoint real-address model evaluation; source and derived-model provenance are recorded in LICENSING.md", - "dataset": "evaluation/legacy_reference_500.jsonl", - "dataset_sha256": "853916e36cfc5a52d06add0524ba64ca2c771073c1c4e98cd5645116e7f74588", - "corpus": { - "examples": 429, - "groups": 108, - "splits": { - "train": { - "examples": 301, - "groups": 77, - "tokens": 498, - "positive_sequences": 272, - "views": { - "hierarchy_without_markers": 74, - "locality_and_street": 11, - "locality_only": 58, - "observed_residual": 83, - "street_only": 75 - }, - "labels": { - "CITY": 189, - "DISTRICT": 3, - "O": 77, - "REGION": 15, - "SETTLEMENT": 6, - "STREET": 208 - } - }, - "validation": { - "examples": 58, - "groups": 14, - "tokens": 101, - "positive_sequences": 50, - "views": { - "hierarchy_without_markers": 14, - "locality_and_street": 3, - "locality_only": 13, - "observed_residual": 16, - "street_only": 12 - }, - "labels": { - "CITY": 29, - "DISTRICT": 2, - "O": 16, - "REGION": 4, - "SETTLEMENT": 3, - "STREET": 47 - } - }, - "test": { - "examples": 70, - "groups": 17, - "tokens": 115, - "positive_sequences": 62, - "views": { - "hierarchy_without_markers": 17, - "locality_and_street": 2, - "locality_only": 16, - "observed_residual": 21, - "street_only": 14 - }, - "labels": { - "CITY": 45, - "O": 20, - "REGION": 3, - "STREET": 47 - } - } - }, - "leaking_groups": [] - }, - "tuning": [ - { - "epochs": 5, - "model_bytes": 32646, - "validation": { - "examples": 58, - "tokens": 101, - "token_accuracy": 0.821782, - "sequence_accuracy": 0.775862, - "macro_entity_f1": 0.423816, - "micro_entity_f1": 0.811765, - "labels": { - "O": { - "tp": 14, - "fp": 2, - "fn": 2, - "support": 16, - "precision": 0.875, - "recall": 0.875, - "f1": 0.875 - }, - "REGION": { - "tp": 1, - "fp": 0, - "fn": 3, - "support": 4, - "precision": 1.0, - "recall": 0.25, - "f1": 0.4 - }, - "DISTRICT": { - "tp": 0, - "fp": 0, - "fn": 2, - "support": 2, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "CITY": { - "tp": 24, - "fp": 1, - "fn": 5, - "support": 29, - "precision": 0.96, - "recall": 0.827586, - "f1": 0.888889 - }, - "SETTLEMENT": { - "tp": 0, - "fp": 0, - "fn": 3, - "support": 3, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "STREET": { - "tp": 44, - "fp": 15, - "fn": 3, - "support": 47, - "precision": 0.745763, - "recall": 0.93617, - "f1": 0.830189 - } - } - } - }, - { - "epochs": 10, - "model_bytes": 33434, - "validation": { - "examples": 58, - "tokens": 101, - "token_accuracy": 0.821782, - "sequence_accuracy": 0.741379, - "macro_entity_f1": 0.541778, - "micro_entity_f1": 0.826347, - "labels": { - "O": { - "tp": 14, - "fp": 5, - "fn": 2, - "support": 16, - "precision": 0.736842, - "recall": 0.875, - "f1": 0.8 - }, - "REGION": { - "tp": 4, - "fp": 0, - "fn": 0, - "support": 4, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "DISTRICT": { - "tp": 0, - "fp": 0, - "fn": 2, - "support": 2, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "CITY": { - "tp": 24, - "fp": 1, - "fn": 5, - "support": 29, - "precision": 0.96, - "recall": 0.827586, - "f1": 0.888889 - }, - "SETTLEMENT": { - "tp": 0, - "fp": 0, - "fn": 3, - "support": 3, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "STREET": { - "tp": 41, - "fp": 12, - "fn": 6, - "support": 47, - "precision": 0.773585, - "recall": 0.87234, - "f1": 0.82 - } - } - } - }, - { - "epochs": 20, - "model_bytes": 33971, - "validation": { - "examples": 58, - "tokens": 101, - "token_accuracy": 0.811881, - "sequence_accuracy": 0.724138, - "macro_entity_f1": 0.535961, - "micro_entity_f1": 0.814371, - "labels": { - "O": { - "tp": 14, - "fp": 5, - "fn": 2, - "support": 16, - "precision": 0.736842, - "recall": 0.875, - "f1": 0.8 - }, - "REGION": { - "tp": 4, - "fp": 0, - "fn": 0, - "support": 4, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "DISTRICT": { - "tp": 0, - "fp": 0, - "fn": 2, - "support": 2, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "CITY": { - "tp": 23, - "fp": 1, - "fn": 6, - "support": 29, - "precision": 0.958333, - "recall": 0.793103, - "f1": 0.867925 - }, - "SETTLEMENT": { - "tp": 0, - "fp": 0, - "fn": 3, - "support": 3, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "STREET": { - "tp": 41, - "fp": 13, - "fn": 6, - "support": 47, - "precision": 0.759259, - "recall": 0.87234, - "f1": 0.811881 - } - } - } - }, - { - "epochs": 40, - "model_bytes": 34672, - "validation": { - "examples": 58, - "tokens": 101, - "token_accuracy": 0.821782, - "sequence_accuracy": 0.741379, - "macro_entity_f1": 0.541629, - "micro_entity_f1": 0.826347, - "labels": { - "O": { - "tp": 14, - "fp": 5, - "fn": 2, - "support": 16, - "precision": 0.736842, - "recall": 0.875, - "f1": 0.8 - }, - "REGION": { - "tp": 4, - "fp": 0, - "fn": 0, - "support": 4, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "DISTRICT": { - "tp": 0, - "fp": 0, - "fn": 2, - "support": 2, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "CITY": { - "tp": 23, - "fp": 0, - "fn": 6, - "support": 29, - "precision": 1.0, - "recall": 0.793103, - "f1": 0.884615 - }, - "SETTLEMENT": { - "tp": 0, - "fp": 0, - "fn": 3, - "support": 3, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "STREET": { - "tp": 42, - "fp": 13, - "fn": 5, - "support": 47, - "precision": 0.763636, - "recall": 0.893617, - "f1": 0.823529 - } - } - } - }, - { - "epochs": 80, - "model_bytes": 35456, - "validation": { - "examples": 58, - "tokens": 101, - "token_accuracy": 0.80198, - "sequence_accuracy": 0.706897, - "macro_entity_f1": 0.511363, - "micro_entity_f1": 0.802395, - "labels": { - "O": { - "tp": 14, - "fp": 5, - "fn": 2, - "support": 16, - "precision": 0.736842, - "recall": 0.875, - "f1": 0.8 - }, - "REGION": { - "tp": 4, - "fp": 1, - "fn": 0, - "support": 4, - "precision": 0.8, - "recall": 1.0, - "f1": 0.888889 - }, - "DISTRICT": { - "tp": 0, - "fp": 0, - "fn": 2, - "support": 2, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "CITY": { - "tp": 23, - "fp": 1, - "fn": 6, - "support": 29, - "precision": 0.958333, - "recall": 0.793103, - "f1": 0.867925 - }, - "SETTLEMENT": { - "tp": 0, - "fp": 0, - "fn": 3, - "support": 3, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "STREET": { - "tp": 40, - "fp": 13, - "fn": 7, - "support": 47, - "precision": 0.754717, - "recall": 0.851064, - "f1": 0.8 - } - } - } - } - ], - "selected_epochs": 10, - "test_sequence": { - "synthetic_starter": { - "examples": 70, - "tokens": 115, - "token_accuracy": 0.713043, - "sequence_accuracy": 0.614286, - "macro_entity_f1": 0.808777, - "micro_entity_f1": 0.780952, - "labels": { - "O": { - "tp": 0, - "fp": 0, - "fn": 20, - "support": 20, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "REGION": { - "tp": 3, - "fp": 1, - "fn": 0, - "support": 3, - "precision": 0.75, - "recall": 1.0, - "f1": 0.857143 - }, - "DISTRICT": { - "tp": 0, - "fp": 2, - "fn": 0, - "support": 0, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "CITY": { - "tp": 45, - "fp": 23, - "fn": 0, - "support": 45, - "precision": 0.661765, - "recall": 1.0, - "f1": 0.79646 - }, - "SETTLEMENT": { - "tp": 0, - "fp": 0, - "fn": 0, - "support": 0, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "STREET": { - "tp": 34, - "fp": 7, - "fn": 13, - "support": 47, - "precision": 0.829268, - "recall": 0.723404, - "f1": 0.772727 - } - }, - "failure_sample": [ - { - "id": "legacy-good-0761:street_only", - "view": "street_only", - "tokens": [ - "Варшавское" - ], - "expected": [ - "STREET" - ], - "actual": [ - "CITY" - ] - }, - { - "id": "legacy-good-0563", - "view": "observed_residual", - "tokens": [ - "ком" - ], - "expected": [ - "O" - ], - "actual": [ - "STREET" - ] - }, - { - "id": "legacy-good-0585", - "view": "observed_residual", - "tokens": [ - "Москва", - "Москва", - "г" - ], - "expected": [ - "CITY", - "O", - "O" - ], - "actual": [ - "CITY", - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0763", - "view": "observed_residual", - "tokens": [ - "Москва", - "ул" - ], - "expected": [ - "CITY", - "O" - ], - "actual": [ - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0763:street_only", - "view": "street_only", - "tokens": [ - "Ленинский" - ], - "expected": [ - "STREET" - ], - "actual": [ - "DISTRICT" - ] - }, - { - "id": "legacy-good-0160", - "view": "observed_residual", - "tokens": [ - "шоссе" - ], - "expected": [ - "O" - ], - "actual": [ - "CITY" - ] - }, - { - "id": "legacy-good-0753", - "view": "observed_residual", - "tokens": [ - "уп", - "Адмирала", - "Макарова" - ], - "expected": [ - "O", - "STREET", - "STREET" - ], - "actual": [ - "CITY", - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0753:hierarchy_without_markers", - "view": "hierarchy_without_markers", - "tokens": [ - "Москва", - "Адмирала", - "Макарова" - ], - "expected": [ - "CITY", - "STREET", - "STREET" - ], - "actual": [ - "CITY", - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0753:street_only", - "view": "street_only", - "tokens": [ - "Адмирала", - "Макарова" - ], - "expected": [ - "STREET", - "STREET" - ], - "actual": [ - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0647", - "view": "observed_residual", - "tokens": [ - "ул" - ], - "expected": [ - "O" - ], - "actual": [ - "CITY" - ] - }, - { - "id": "legacy-good-0335", - "view": "observed_residual", - "tokens": [ - "г", - "Москва" - ], - "expected": [ - "O", - "CITY" - ], - "actual": [ - "CITY", - "CITY" - ] - }, - { - "id": "legacy-good-0947", - "view": "observed_residual", - "tokens": [ - "Соколово", - "Мещерская" - ], - "expected": [ - "STREET", - "STREET" - ], - "actual": [ - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0947:hierarchy_without_markers", - "view": "hierarchy_without_markers", - "tokens": [ - "Москва", - "Соколово", - "Мещерская" - ], - "expected": [ - "CITY", - "STREET", - "STREET" - ], - "actual": [ - "CITY", - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0353", - "view": "observed_residual", - "tokens": [ - "Москва", - "г" - ], - "expected": [ - "CITY", - "O" - ], - "actual": [ - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0353:street_only", - "view": "street_only", - "tokens": [ - "Николоямская" - ], - "expected": [ - "STREET" - ], - "actual": [ - "REGION" - ] - }, - { - "id": "legacy-good-0764", - "view": "observed_residual", - "tokens": [ - "Московская", - "Подольск", - "г" - ], - "expected": [ - "REGION", - "CITY", - "O" - ], - "actual": [ - "REGION", - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0358", - "view": "observed_residual", - "tokens": [ - "ул" - ], - "expected": [ - "O" - ], - "actual": [ - "CITY" - ] - }, - { - "id": "legacy-good-0391", - "view": "observed_residual", - "tokens": [ - "Москва", - "Москва", - "Академика", - "Королева" - ], - "expected": [ - "CITY", - "O", - "STREET", - "STREET" - ], - "actual": [ - "CITY", - "CITY", - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0391:hierarchy_without_markers", - "view": "hierarchy_without_markers", - "tokens": [ - "Москва", - "Академика", - "Королева" - ], - "expected": [ - "CITY", - "STREET", - "STREET" - ], - "actual": [ - "CITY", - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0391:street_only", - "view": "street_only", - "tokens": [ - "Академика", - "Королева" - ], - "expected": [ - "STREET", - "STREET" - ], - "actual": [ - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0796", - "view": "observed_residual", - "tokens": [ - "г", - "ул", - "д" - ], - "expected": [ - "O", - "O", - "O" - ], - "actual": [ - "CITY", - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0349", - "view": "observed_residual", - "tokens": [ - "этаж" - ], - "expected": [ - "O" - ], - "actual": [ - "CITY" - ] - }, - { - "id": "legacy-good-0349:street_only", - "view": "street_only", - "tokens": [ - "Космонавтов" - ], - "expected": [ - "STREET" - ], - "actual": [ - "CITY" - ] - }, - { - "id": "legacy-good-0814", - "view": "observed_residual", - "tokens": [ - "г" - ], - "expected": [ - "O" - ], - "actual": [ - "CITY" - ] - }, - { - "id": "legacy-good-0807", - "view": "observed_residual", - "tokens": [ - "Кронверкский", - "пр" - ], - "expected": [ - "STREET", - "O" - ], - "actual": [ - "DISTRICT", - "STREET" - ] - }, - { - "id": "legacy-good-0049", - "view": "observed_residual", - "tokens": [ - "Проспект" - ], - "expected": [ - "O" - ], - "actual": [ - "CITY" - ] - }, - { - "id": "legacy-good-0379", - "view": "observed_residual", - "tokens": [ - "Москва", - "Москва" - ], - "expected": [ - "CITY", - "O" - ], - "actual": [ - "CITY", - "CITY" - ] - } - ] - }, - "real_model": { - "examples": 70, - "tokens": 115, - "token_accuracy": 0.965217, - "sequence_accuracy": 0.942857, - "macro_entity_f1": 0.978495, - "micro_entity_f1": 0.96875, - "labels": { - "O": { - "tp": 18, - "fp": 0, - "fn": 2, - "support": 20, - "precision": 1.0, - "recall": 0.9, - "f1": 0.947368 - }, - "REGION": { - "tp": 3, - "fp": 0, - "fn": 0, - "support": 3, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "DISTRICT": { - "tp": 0, - "fp": 0, - "fn": 0, - "support": 0, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "CITY": { - "tp": 45, - "fp": 3, - "fn": 0, - "support": 45, - "precision": 0.9375, - "recall": 1.0, - "f1": 0.967742 - }, - "SETTLEMENT": { - "tp": 0, - "fp": 0, - "fn": 0, - "support": 0, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "STREET": { - "tp": 45, - "fp": 1, - "fn": 2, - "support": 47, - "precision": 0.978261, - "recall": 0.957447, - "f1": 0.967742 - } - }, - "failure_sample": [ - { - "id": "legacy-good-0753", - "view": "observed_residual", - "tokens": [ - "уп", - "Адмирала", - "Макарова" - ], - "expected": [ - "O", - "STREET", - "STREET" - ], - "actual": [ - "O", - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0753:street_only", - "view": "street_only", - "tokens": [ - "Адмирала", - "Макарова" - ], - "expected": [ - "STREET", - "STREET" - ], - "actual": [ - "CITY", - "STREET" - ] - }, - { - "id": "legacy-good-0391", - "view": "observed_residual", - "tokens": [ - "Москва", - "Москва", - "Академика", - "Королева" - ], - "expected": [ - "CITY", - "O", - "STREET", - "STREET" - ], - "actual": [ - "CITY", - "CITY", - "STREET", - "STREET" - ] - }, - { - "id": "legacy-good-0379", - "view": "observed_residual", - "tokens": [ - "Москва", - "Москва" - ], - "expected": [ - "CITY", - "O" - ], - "actual": [ - "CITY", - "STREET" - ] - } - ] - } - }, - "test_end_to_end": { - "rows": 21, - "synthetic_starter": { - "rows": 21, - "review_statuses": { - "legacy_reference_not_independently_rereviewed": 21 - }, - "metric_definitions": { - "exact_address_rate": "fraction of rows where every public component value matches", - "no_unparsed_rate": "fraction of rows with no residual word or number spans", - "exact_component_value_micro": "micro precision, recall, and F1 over case-insensitive exact component values after whitespace and ё/е folding" - }, - "exact_address_rate": 0.428571, - "no_unparsed_rate": 0.285714, - "exact_component_value_micro": { - "tp": 101, - "fp": 11, - "fn": 18, - "precision": 0.901786, - "recall": 0.848739, - "f1": 0.874459 - }, - "micro": { - "tp": 101, - "fp": 11, - "fn": 18, - "precision": 0.901786, - "recall": 0.848739, - "f1": 0.874459 - }, - "fields": { - "postal_code": { - "tp": 21, - "fp": 0, - "fn": 0, - "support": 21, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "region": { - "tp": 2, - "fp": 0, - "fn": 0, - "support": 2, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "district": { - "tp": 0, - "fp": 1, - "fn": 0, - "support": 0, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "city": { - "tp": 18, - "fp": 3, - "fn": 3, - "support": 21, - "precision": 0.857143, - "recall": 0.857143, - "f1": 0.857143 - }, - "settlement": { - "tp": 0, - "fp": 0, - "fn": 0, - "support": 0, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "street": { - "tp": 17, - "fp": 4, - "fn": 4, - "support": 21, - "precision": 0.809524, - "recall": 0.809524, - "f1": 0.809524 - }, - "street_type": { - "tp": 13, - "fp": 2, - "fn": 8, - "support": 21, - "precision": 0.866667, - "recall": 0.619048, - "f1": 0.722222 - }, - "house_num": { - "tp": 20, - "fp": 1, - "fn": 1, - "support": 21, - "precision": 0.952381, - "recall": 0.952381, - "f1": 0.952381 - }, - "corpus": { - "tp": 2, - "fp": 0, - "fn": 0, - "support": 2, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "structure": { - "tp": 5, - "fp": 0, - "fn": 0, - "support": 5, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "apartment": { - "tp": 3, - "fp": 0, - "fn": 2, - "support": 5, - "precision": 1.0, - "recall": 0.6, - "f1": 0.75 - } - } - }, - "real_model": { - "rows": 21, - "review_statuses": { - "legacy_reference_not_independently_rereviewed": 21 - }, - "metric_definitions": { - "exact_address_rate": "fraction of rows where every public component value matches", - "no_unparsed_rate": "fraction of rows with no residual word or number spans", - "exact_component_value_micro": "micro precision, recall, and F1 over case-insensitive exact component values after whitespace and ё/е folding" - }, - "exact_address_rate": 0.52381, - "no_unparsed_rate": 0.238095, - "exact_component_value_micro": { - "tp": 105, - "fp": 6, - "fn": 14, - "precision": 0.945946, - "recall": 0.882353, - "f1": 0.913043 - }, - "micro": { - "tp": 105, - "fp": 6, - "fn": 14, - "precision": 0.945946, - "recall": 0.882353, - "f1": 0.913043 - }, - "fields": { - "postal_code": { - "tp": 21, - "fp": 0, - "fn": 0, - "support": 21, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "region": { - "tp": 2, - "fp": 0, - "fn": 0, - "support": 2, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "district": { - "tp": 0, - "fp": 0, - "fn": 0, - "support": 0, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "city": { - "tp": 20, - "fp": 1, - "fn": 1, - "support": 21, - "precision": 0.952381, - "recall": 0.952381, - "f1": 0.952381 - }, - "settlement": { - "tp": 0, - "fp": 0, - "fn": 0, - "support": 0, - "precision": 0.0, - "recall": 0.0, - "f1": 0.0 - }, - "street": { - "tp": 19, - "fp": 2, - "fn": 2, - "support": 21, - "precision": 0.904762, - "recall": 0.904762, - "f1": 0.904762 - }, - "street_type": { - "tp": 13, - "fp": 2, - "fn": 8, - "support": 21, - "precision": 0.866667, - "recall": 0.619048, - "f1": 0.722222 - }, - "house_num": { - "tp": 20, - "fp": 1, - "fn": 1, - "support": 21, - "precision": 0.952381, - "recall": 0.952381, - "f1": 0.952381 - }, - "corpus": { - "tp": 2, - "fp": 0, - "fn": 0, - "support": 2, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "structure": { - "tp": 5, - "fp": 0, - "fn": 0, - "support": 5, - "precision": 1.0, - "recall": 1.0, - "f1": 1.0 - }, - "apartment": { - "tp": 3, - "fp": 0, - "fn": 2, - "support": 5, - "precision": 1.0, - "recall": 0.6, - "f1": 0.75 - } - } - } - }, - "model_bytes": { - "synthetic_starter": 20106, - "real_model": 37130 - } -} diff --git a/upload_fias.py b/upload_fias.py deleted file mode 100644 index f576613..0000000 --- a/upload_fias.py +++ /dev/null @@ -1,197 +0,0 @@ -# DISCLAIMER -# Я настоятельно рекомендую запускать этот скрипт построчно из Jupyter notebook. -# 1. Он очень долгий. Перегон dfb в csv занимает где-то 20 минут, загрузка названий улиц ещё 2, а номера домов это ещё часов на 8-10 -# 2. Требует очень много памяти и можно не заметить как на машине она закончится. Таблицы с csv занимают около 50Гб, в elastic это может весить ещё около 100гб. Удаляйте dfb после того как получили csv файлы. - - -import os -import csv -import glob -import shutil -import argparse - -import pandas as pd -from elasticsearch import Elasticsearch, helpers -from simpledbf import Dbf5 - -pd.options.display.max_columns = None - -es = Elasticsearch() - -def load_elastic(fn, index, doc_type, encoding='cp866', es=es): - ''' - Этот метод загружает указанный файлик в elastic. - ''' - with open(fn, encoding=encoding) as f: - reader = csv.DictReader(f) - helpers.bulk(es, reader, index=index, doc_type=doc_type, raise_on_error=False, stats_only=True) - print('done') - - -# # Оптимизация под полнотекстовый поиск -def full_address(GUID): - answer = es.search(index='fias', doc_type='address', body= - { - "size": 1, - "query": { - "bool": { - "must": [ - {"match": { - "AOGUID": GUID}}, - {"match": { - "ACTSTATUS": 1}} - ] - } - } - }) - entry = answer["hits"]["hits"][0]["_source"] - string = entry['SHORTNAME'] + " " + entry['OFFNAME'] - if len(entry['PARENTGUID']) > 5: - string = full_address(entry['PARENTGUID']) + ', ' + string - return string - - -def full_address_sep(GUID, _leaf=True): - address = {} - address['fullname'] = '' - answer = es.search(index='fias', doc_type='address', body= - { - "size": 1, - "query": { - "bool": { - "must": [ - {"match": { - "AOGUID": GUID}}, - {"match": { - "ACTSTATUS": 1}} - ] - } - } - }) - - try: - entry = answer["hits"]["hits"][0]["_source"] - level = entry["AOLEVEL"] - LUT = { - '1': 'region', - '2': 'aregion', - '3': 'area', - '4': 'city', - '5': 'district', - '6': 'town', - '7': 'street', - '8': 'building', - '9': 'placement', - '65': 'planning', - '75': 'land', - '90': 'additional', - '91': 'nestreet' - } - if _leaf: - address['guid'] = GUID - address['aolevel'] = entry['AOLEVEL'] - address[LUT.get(level, level)] = entry['OFFNAME'] - address[LUT.get(level, level) + "_type"] = entry['SHORTNAME'] - address['fullname'] = entry['SHORTNAME'] + " " + entry['OFFNAME'] - if len(entry['PARENTGUID']) > 5: - nest = full_address_sep(entry['PARENTGUID'], _leaf=False) - string = nest['fullname'] + ', ' + address['fullname'] - address.update(nest) - address['fullname'] = string - except Exception: - print("failed get address") - print(answer) - - return address - - -if __name__ == "__main__": - - parser = argparse.ArgumentParser() - parser.add_argument('--fiasdir', default='') - parser.add_argument('--remove', default=False, action='store_true') - parser.add_argument('--dont-remove', dest='remove', action='store_false') - args = parser.parse_args() - fias_dir = os.path.join(args.fiasdir, 'fias_dbf/'); - fias_csv_dir = os.path.dirname(args.fiasdir + 'fias_csv/') - - files = glob.glob(os.path.join(fias_dir, 'ADDR*'), recursive=True) - - # # Для начала преобразуем всё в csv - - os.makedirs('fias_csv', exist_ok=True) - - files = glob.glob(os.path.join(fias_dir, 'ADDR*'), recursive=True) - for i, f in enumerate(files): - if f[-3:].lower() == 'dbf': - print('processing {0} of {1}. Filename: {2} '.format(i + 1, len(files), f), end='\r') - dbf = Dbf5(f, codec='cp866') - dbf.to_csv('fias_csv/ADDROBJ.csv') - if args.remove: - os.remove(f) # delete to save memory - - -# files = ['ESTSTAT.DBF', 'FLATTYPE.DBF', 'HSTSTAT.DBF', 'INTVSTAT.DBF', 'NDOCTYPE.DBF', -# 'OPERSTAT.DBF', 'ROOMTYPE.DBF', 'SOCRBASE.DBF', 'STRSTAT.DBF'] -# for i, f in enumerate(files): -# if f[-3:].lower() == 'dbf': -# print('processing {0} of {1}. Filename: {2} '.format(i + 1, len(files), f), end='\r') -# dbf = Dbf5(os.path.join(fias_dir, f), codec='cp866') -# dbf.to_csv('fias_csv/{0}.csv'.format(f[:-4])) - - files = glob.glob(os.path.join(fias_dir, 'HOUSE*'), recursive=True) - for i, f in enumerate(files): - if f[-3:].lower() == 'dbf': - print('processing {0} of {1}. Filename: {2} '.format(i + 1, len(files), f), end='\r') - dbf = Dbf5(f, codec='cp866') - dbf.to_csv('fias_csv/HOUSE.csv') - if args.remove: - os.remove(f) # delete to save memory - -# files = glob.glob(os.path.join(fias_dir, 'ROOM*'), recursive=True) -# for i, f in enumerate(files): -# if f[-3:].lower() == 'dbf': -# print('processing {0} of {1}. Filename: {2} '.format(i + 1, len(files), f), end='\r') -# dbf = Dbf5(f, codec='cp866') -# dbf.to_csv('fias_csv/ROOM.csv') - -# files = glob.glob(os.path.join(fias_dir, 'STEAD*'), recursive=True) -# for i, f in enumerate(files): -# if f[-3:].lower() == 'dbf': -# print('processing {0} of {1}. Filename: {2} '.format(i + 1, len(files), f), end='\r') -# dbf = Dbf5(f, codec='cp866') -# dbf.to_csv('fias_csv/STEAD.csv') - - # # Теперь надо всё это закинуть в Elastic - # Да, это не самый оптимальный путь (можно миновать csv). Но это уже как есть - - - # Загрузка самой главной таблицы - # На первых порах её нам хватит. Остальные загружаются при надобности - # Занимает 2 часа - # FIXME - # load_elastic(os.path.join(fias_csv_dir, 'ADDROBJ.csv'), 'fias', 'address') - - # Загрузка в полнотекстовый поиск, где есть и адрес и город и индекс - df_addr = pd.read_csv(os.path.join(fias_csv_dir, 'ADDROBJ.csv'), encoding='cp866', dtype=str, error_bad_lines=False) - # здесь могут быть ошибки парсинга на некоторых полях. - # Их можно просто пропустить а потом попытаться исправить самостоятельно. - start = 0 - finish = None - i = start - for _, value in df_addr[["AOGUID"]][df_addr['ACTSTATUS'] == '1'][start:finish].iterrows(): - if i % 50 == 0: - print(i, end="\r") - - full_addr = full_address_sep(value["AOGUID"]) - # print(full_addr) - es.index(index="fias_full_text", id=value["AOGUID"], doc_type='address', body=full_addr) - i += 1 - - # На данном этапе в elastic должна быть таблица fias_full_text. Далее мы её будем максимально активно использовать - - # Можно ставить на ночь. Это очень долго: 18Гб таблица весит - load_elastic(os.path.join(fias_csv_dir, 'HOUSE.csv'), 'fias_houses', 'home') - - # Удаляем все csv-таблицы, они теперь есть в elastic - shutil.rmtree("fias_csv")