structured-address-fix: ISO 20022 Structured-Address Remediation
A vendor-neutral core library that detects, scores, and remediates
non-compliant postal addresses in ISO 20022 payment messages —
ahead of the 14 November 2026 cliff, when fully unstructured addresses
are rejected across SWIFT CBPR+, HVPS+, T2 RTGS, CHAPS, Fedwire, and Lynx.
Point it at a pacs.008 or pain.001 document, get back an explainable
list of findings, and turn an AdrLine-only address into a structured one
with reversible, auditable patches.
The cliff, in one line. From 14 November 2026 an ISO 20022 postal address that is
AdrLine-only (unstructured) is non-compliant and liable to be rejected by the major cross-border and high-value schemes. A hybrid address (town + country plus at most two residualAdrLinelines) is the minimum bar; structured is the target. This library exists to move addresses across that line before it bites.
Contents
- Overview
- The ISO 20022 Suite
- Install
- Quick Start
- Architecture
- Policies
- Premium rule packs
- Writing a rule pack
- When not to use structured-address-fix
- Development
- Security
- Documentation
- License
- Contributing
- Acknowledgements
Overview
structured-address-fix is the shared core of a small ISO 20022 address
toolchain. It reads the postal addresses carried by the parties of a payment
message (debtor, creditor, agents, ultimate parties), classifies each one as
structured, hybrid, or unstructured per CBPR+ UG2026, scores the
compliance risk against a chosen policy, and — where it can — proposes a
remediation that promotes the address toward the structured target.
Two properties are non-negotiable:
-
Explainable. Every remediation is a set of RFC 6902-shaped
PatchOperations. Each records why it exists (theFindingCodeit resolves), where its value came from (the originatingAdrLinetoken, when applicable), and how confident the deriving heuristic was. Anything it cannot resolve is reported as a residual finding, so the gap to full compliance is always visible. -
Safe by default. Untrusted XML is parsed with
defusedxml(no XXE, no billion-laughs). The canonical address model is frozen, closed to extra fields, and enforces the ISO 20022 element-length maxima as construction invariants. Errors raise a typedStructuredAddressErrorwith a stablecodeand safecontext. -
Website / docs: https://sebastienrousseau.github.io/structured-address-fix/
-
Source code: https://github.com/sebastienrousseau/structured-address-fix
-
Bug reports: https://github.com/sebastienrousseau/structured-address-fix/issues
The public surface is the structured_address_fix.services facade; the
domain models are re-exported from the package root for convenience.
The ISO 20022 Suite
structured-address-fix is the postal-address remediation core of the
sebastienrousseau ISO 20022 suite. It supersedes and generalises the bespoke
pacs008.standards.address module, and it is the shared engine that a thin
Model Context Protocol server wraps for AI agents — so the classification and
remediation logic lives in exactly one place and every surface behaves
identically.
| Package | Role |
|---|---|
structured-address-fix |
The core library (this package): domain model, policies, adapters, services facade, plugin SDK |
structured-address-fix-mcp |
Thin Model Context Protocol server wrapping the services facade for AI agents |
pacs008 |
FI-to-FI credit-transfer library; delegates its address linting + Nov-2026 readiness here |
flowchart LR
A["MCP / REST / LSP wrapper"] -->|delegates to| B["structured_address_fix.services"]
B -->|classify + score + remediate| C["ISO 20022 pacs.008 / pain.001 XML"]
Install
structured-address-fix runs on macOS, Linux, and Windows and requires
Python 3.12+ and pip. Its only runtime dependencies are pydantic,
defusedxml, and xmlschema.
python -m pip install structured-address-fix
Using an isolated virtual environment (recommended)
python -m venv venv
source venv/bin/activate # macOS/Linux
venv\Scripts\activate # Windows
python -m pip install -U structured-address-fix
Quick Start
For the guided tour, see docs/quickstart.md.
Classify an address
The domain model is re-exported from the package root and works standalone.
CanonicalAddress models the ISO 20022 PostalAddress27 type and classifies
itself per CBPR+ UG2026:
from structured_address_fix import CanonicalAddress
# Unstructured: AdrLine only, no town / country -> rejected from the cliff.
unstructured = CanonicalAddress(
address_lines=("221B Baker Street", "London NW1 6XE", "United Kingdom"),
)
print(unstructured.classification) # AddressClassification.UNSTRUCTURED
# Structured: town + country + structured detail, no residual AdrLine.
structured = CanonicalAddress(
building_number="221B",
street_name="Baker Street",
post_code="NW1 6XE",
town_name="London",
country="GB",
)
print(structured.classification) # AddressClassification.STRUCTURED
# Serialise back to ISO 20022 element names for the wire.
structured.model_dump(by_alias=True, exclude_none=True)
# {'BldgNb': '221B', 'StrtNm': 'Baker Street', 'PstCd': 'NW1 6XE',
# 'TwnNm': 'London', 'Ctry': 'GB', ...}
Assess and remediate a message
The top-level entry point is the structured_address_fix.services facade:
assess returns a read-only ValidationReport;
remediate adds the proposed fixes (and, optionally, the patched XML):
from structured_address_fix.services import assess, remediate
pacs008_xml = ... # an ISO 20022 pacs.008 document, as text
# 1. Score every party's address against a policy.
report = assess(pacs008_xml, policy="cbpr-2026")
print(report.is_compliant) # False
for finding in report.findings:
print(finding.code, finding.party_role, finding.message)
# SAF001 Dbtr Address is AdrLine-only; rejected from 2026-11-14
# 2. Propose reversible, explainable patches for what can be fixed.
result = remediate(pacs008_xml, policy="cbpr-2026")
for suggestion in result.suggestions:
print(suggestion.before.classification, "->",
suggestion.after.classification) # unstructured -> hybrid
for op in suggestion.operations:
print(op.op, op.path, op.reason_code, op.confidence)
# Anything no heuristic could resolve is reported, never hidden:
for residual in suggestion.residual_findings:
print("residual:", residual.code)
The built-in policies, XML adapters, per-country heuristics, and the
assess/remediatefacade land in the v0.0.x series; seeROADMAP.md. The domain model and error taxonomy shipped in v0.0.2 and are stable.
Architecture
The library is layered so that each concern is testable in isolation and the public surface stays small. Dependencies point downward only.
| Layer | Module | Responsibility |
|---|---|---|
| Domain | structured_address_fix.domain |
Pure Pydantic v2 entities and their invariants, with zero I/O: CanonicalAddress, AddressedParty, RiskFinding, PatchOperation, the result envelopes, and the wire-facing enums. |
| Policies | structured_address_fix.policies |
Rulebooks that inspect a CanonicalAddress and raise RiskFindings: the four built-ins (see below), each citing a rulebook clause. |
| Adapters | structured_address_fix.adapters |
defusedxml parsing of pacs.008 / pain.001, party discovery, and the per-country AdrLine → structured heuristics that populate the canonical model with recorded confidence + source tokens. |
| Services | structured_address_fix.services |
The assess / remediate facade — the single public entry point wrappers (MCP / REST / LSP) call. |
| Plugins | structured_address_fix.plugins |
The premium rule-pack SDK: entry-point discovery of third-party PolicyPacks, gated by entitlement + integrity checks. |
Policies
A policy is a rulebook: it names the finding codes that apply and the severity each carries. Four are built in and open-source:
| Policy id | Scope | Use it when |
|---|---|---|
cbpr-2026 |
SWIFT CBPR+ UG2026 cross-border requirements — the Nov-2026 cliff rules. The default. | You send or receive cross-border SWIFT payments |
sepa |
SEPA credit-transfer address expectations | You operate in the euro retail-payment area |
hvps-plus |
HVPS+ high-value / RTGS market-practice (T2, CHAPS, Fedwire, Lynx) | You settle over a high-value system |
generic-structured |
Scheme-agnostic structured-vs-unstructured hygiene | You want the baseline structural checks with no scheme assumptions |
The default policy is cbpr-2026, overridable per call or via the
SAF_DEFAULT_POLICY environment variable. Finding codes (SAF001–SAF008)
are a stable public API: a code's meaning is fixed once released. See
docs/policies.md for the full rule-to-code mapping and
docs/error-taxonomy.md for the exception codes.
Premium rule packs
The four built-in policies cover the open, published scheme rules. Some requirements — proprietary market-practice profiles, correspondent-specific overlays, jurisdiction-specific address validation — are better shipped as premium rule packs: separately installable Python distributions that register additional policies through an entry point.
A pack is discovered automatically once installed, contributes its own policy
ids and finding codes (with their own prefixes, so they never collide with the
core SAF codes), and is gated at load time by the entitlement + integrity
checks in the error taxonomy
(PackNotLicensedError, PackIntegrityError). The core never bundles a pack;
you opt in by installing one.
from structured_address_fix.services import assess
# A premium pack's policy id, resolved through the plugin registry.
report = assess(message_xml, policy="acme-correspondent-2026")
Writing a rule pack
Publishing a rule pack means shipping a PolicyPack and advertising it under
the structured_address_fix.policies entry-point group:
# pyproject.toml of your pack distribution
[project.entry-points."structured_address_fix.policies"]
acme-correspondent-2026 = "acme_saf_pack:PACK"
# acme_saf_pack/__init__.py
from structured_address_fix.domain import (
FindingCode, RiskFinding, Severity, CanonicalAddress,
)
def check(address: CanonicalAddress, *, location: str) -> list[RiskFinding]:
"""Raise the pack's findings against a single address."""
findings: list[RiskFinding] = []
if address.post_code is None:
findings.append(RiskFinding(
code=FindingCode.MISSING_TOWN, # or a pack-specific code
severity=Severity.ERROR,
message="Correspondent profile requires a post code.",
policy_id="acme-correspondent-2026",
location=location,
))
return findings
PACK = ... # a PolicyPack binding the id "acme-correspondent-2026" to check()
The full contract — the PolicyPack shape, custom finding-code prefixes,
entitlement, and integrity signing — is in
docs/writing-a-rule-pack.md.
When not to use structured-address-fix
- Your addresses are already fully structured. If every message you send
already carries
TwnNm+Ctry+ structured detail and noAdrLine, there is nothing to remediate; a one-offassessto confirm compliance is all you need. - You need to generate whole payment messages. This library remediates
the postal-address portion of an existing message; it does not build a
pacs.008 or pain.001 from scratch. Use the message-family libraries for
that (e.g.
pacs008,pain001). - You want an agent tool, not a library import. Use the thin
structured-address-fix-mcpserver, which wraps this facade for MCP clients. - You need authoritative address verification against a postal database.
The
AdrLine→ structured heuristics are deterministic, explainable best-effort splits with recorded confidence; they do not consult a national address file. Low-confidence or unresolvable fragments are reported as residual findings for a human to resolve, never silently guessed. - You need a schema validator. For pure XSD validation of a whole
document, use
xmlschemadirectly; this library focuses on address compliance, not full-message schema conformance.
Development
structured-address-fix uses Poetry and
mise.
git clone https://github.com/sebastienrousseau/structured-address-fix.git && cd structured-address-fix
mise install
poetry install
poetry shell
A Makefile orchestrates the quality gates (kept in lockstep with CI):
make check # lint + type-check + test (REQUIRED before commit)
make test # pytest at 100% line + branch coverage
make lint # ruff + black --check
make type-check # mypy --strict
make security # bandit
make examples # run each examples/*.py
make pip-compile # regenerate hash-pinned requirements/*.txt from *.in
Security
Untrusted ISO 20022 XML is parsed with defusedxml, never the standard-library
parser, so XXE / billion-laughs / external-DTD attacks are structurally
prevented. The canonical model is frozen and extra="forbid", and every error
is a typed StructuredAddressError with a stable code and safe context, so
a wrapping transport can serialise {"error": ...} payloads rather than leaking
tracebacks. Premium rule packs are opt-in and gated by entitlement + integrity
checks. Reporting practice, supported versions, the NIST SSDF practice mapping,
and the accepted OpenSSF Scorecard findings are documented in
SECURITY.md. Vulnerabilities go via GitHub Private
Vulnerability Reporting, not public issues.
Documentation
README.md— this filedocs/index.md— documentation indexdocs/quickstart.md— guided tourdocs/policies.md— the built-in policies and their rulesdocs/writing-a-rule-pack.md— the premium rule-pack contractdocs/remediation-model.md— findings, patches, and resultsdocs/error-taxonomy.md— the exception codesdocs/nov-2026-cutover.md— the Nov-2026 cliff, scheme by schemeCHANGELOG.md— release notesSECURITY.md— disclosure + supported versionsROADMAP.md— where the project is going
License
Licensed under the Apache License, Version 2.0. Any contribution submitted for inclusion shall be licensed as above, without additional terms.
Contributing
Contributions are welcome — see the contributing instructions. Thanks to all contributors.
Acknowledgements
Built on Pydantic, defusedxml, and xmlschema, and grounded in the ISO 20022 CBPR+ / HVPS+ structured-address requirements.
Metadata
Release files for structured-address-fix 0.0.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| structured_address_fix-0.0.4.tar.gz | 52.2 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| structured_address_fix-0.0.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 133.8 kB
Release files / structured_address_fix-0.0.4.tar.gz
| Download URL | structured_address_fix-0.0.4.tar.gz |
|---|---|
| Size | 52.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dc7d43ad3baee7f5c56ff1d85adecea3ac12f1153e0c7495577bafc47dfd9e22
|
|
BLAKE2b-256 checksum How to use checksums |
e779e39d405b5e3a513e34816fa331f2a8a65b9e1e0b7ec3faeb9a050241fcd7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.
Transparency logRelease files / structured_address_fix-0.0.4-py3-none-any.whl
| Download URL | structured_address_fix-0.0.4-py3-none-any.whl |
|---|---|
| Size | 81.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1e943efade0be08c07b2a267bb0bd7bcf7ba1bcfcdc4daaf208f98b1ada28e8f
|
|
BLAKE2b-256 checksum How to use checksums |
4cb09945ee0264a3850c786a8771532351320ea577384b15389bab0b25d077e8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.
Transparency logSecurity for structured-address-fix 0.0.4
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for structured_address_fix-0.0.4.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.