ISO 20022 structured-address remediation and payment-rejection prevention for the November 2026 cliff.
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file structured_address_fix-0.0.2.tar.gz.
File metadata
- Download URL: structured_address_fix-0.0.2.tar.gz
- Upload date:
- Size: 50.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe3258f99fa33358f36c8d43e287cede9be26fc8b232a26788da1b632512a922
|
|
| MD5 |
ac39e58150aea107a3179c4d9f07b163
|
|
| BLAKE2b-256 |
a51f64ab7e894e87b32e438e041bda99ab4e86f0637f59a3937b2b7556346a1c
|
Provenance
The following attestation bundles were made for structured_address_fix-0.0.2.tar.gz:
Publisher:
release.yml on sebastienrousseau/structured-address-fix
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
structured_address_fix-0.0.2.tar.gz -
Subject digest:
fe3258f99fa33358f36c8d43e287cede9be26fc8b232a26788da1b632512a922 - Sigstore transparency entry: 2192350287
- Sigstore integration time:
-
Permalink:
sebastienrousseau/structured-address-fix@9f35e513164bbdf8f55d77e786e928df87507930 -
Branch / Tag:
refs/tags/v0.0.2 - Owner: https://github.com/sebastienrousseau
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9f35e513164bbdf8f55d77e786e928df87507930 -
Trigger Event:
push
-
Statement type:
File details
Details for the file structured_address_fix-0.0.2-py3-none-any.whl.
File metadata
- Download URL: structured_address_fix-0.0.2-py3-none-any.whl
- Upload date:
- Size: 79.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f6eb8ca5dccfe20f65630e882f51e4461690931d9f16740e0619bc27fbb23370
|
|
| MD5 |
cd579b7d556198345c319582db559f47
|
|
| BLAKE2b-256 |
b985e84850a1f1f4a24719fa66eff839bdd83da60752f373d3481eafa3ea6aad
|
Provenance
The following attestation bundles were made for structured_address_fix-0.0.2-py3-none-any.whl:
Publisher:
release.yml on sebastienrousseau/structured-address-fix
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
structured_address_fix-0.0.2-py3-none-any.whl -
Subject digest:
f6eb8ca5dccfe20f65630e882f51e4461690931d9f16740e0619bc27fbb23370 - Sigstore transparency entry: 2192350295
- Sigstore integration time:
-
Permalink:
sebastienrousseau/structured-address-fix@9f35e513164bbdf8f55d77e786e928df87507930 -
Branch / Tag:
refs/tags/v0.0.2 - Owner: https://github.com/sebastienrousseau
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9f35e513164bbdf8f55d77e786e928df87507930 -
Trigger Event:
push
-
Statement type: