Normalize and validate Canadian mailing addresses offline, for forms, CRMs and invoices. Pure Python standard library, no network calls, no dependencies.
Give it 10 - 123 main street, miramichi, new brunswick, e1n1a1 and get back a Canada Post style label, a confidence score and a list of coded issues.
- Postal codes: accepts lowercase, missing space, extra hyphens; outputs
A1A 1A1. Rejects the letters D F I O Q U, rejects W and Z as the first letter, checks the first letter against the province/territory, flags rural FSAs (second character 0). - Provinces and territories: English and French names and codes, with or without accents (
Nouveau-Brunswick,PQ,Terre-Neuve-et-Labrador,N.-B.,P.E.I.). - Street types and directions: English (suffix:
Main Street West->MAIN ST W) and French (prefix:123 rue Principale,Boul.,Ch.,Montee,Rang), with Canada Post abbreviations.AvenuebecomesAVEin English andAVin French;O(ouest) is used in French,Win English. - Units:
Apt,Suite,Unit,#,app,appartement,bureauare moved to the Canada Post position (10-123 MAIN ST). Use--unit-style designatorto keep the word (123 MAIN ST APT 10). - Civic numbers:
123A,123 1/2, and12-345(unit 12 at civic 345). - Non-street delivery: PO Box,
CP/ case postale, RR (with SITE and COMP), General Delivery, Poste restante, withSTN/SUCCstations. - Output: upper-case label without punctuation or accents, plus a readable mixed-case line that keeps accents.
- Per address: confidence 0-1 and issues such as
postal_province_mismatch,invalid_postal_letter,rural_fsa,missing_postal_code,unknown_province,ambiguous_unit.
Python 3.10+. From a clone:
pip install .
or run without installing from the tool folder: python -m canada_address ....
One address, free text (comma or newline separated: street, city, province, postal code):
$ canada-address "Suite 10, 123 main st, Miramichi, New Brunswick, e1n1a1"
10-123 MAIN ST
MIRAMICHI NB E1N 1A1
readable: 10-123 Main St, Miramichi, NB E1N 1A1
confidence: 1.00 (ok)
issues: none
Split fields and JSON output: canada-address --street "45 rue Principale" --unit 4 --city Quebec --province PQ --postal g1k7a1 --json.
A whole CSV:
canada-address --csv messy_addresses.csv -o cleaned_addresses.csv
Columns are detected from the header (address, address2, unit, city, province, postal_code and common variants). Override them with --street-col, --line2-col, --unit-col, --city-col, --province-col, --postal-col, or point --freeform-col at one column holding the whole address. Your original columns are kept; these are added (prefix norm_, change it with --prefix):
street, unit, city, province, postal_code, label, readable, kind, confidence, status, issues, issue_messages
status is ok, warning or error. issues is a ;-separated list of codes.
| Code | Meaning |
|---|---|
| 0 | Done (or no error-level issues with --strict) |
| 1 | --strict and at least one error-level issue, or --min-confidence X and a score below X |
| 2 | Bad arguments, unreadable file, or no address column found |
Warnings (for example missing_civic_number, ambiguous_unit) never fail --strict. See examples/ci-example.yml for a CI step.
from canada_address import normalize_address, parse_address, normalize_postal_code, normalize_province
addr = normalize_address("123 main street suite 10", city="miramichi",
province="new brunswick", postal_code="e1n1a1")
addr.label # "10-123 MAIN ST\nMIRAMICHI NB E1N 1A1"
addr.readable # "10-123 Main St, Miramichi, NB E1N 1A1"
addr.confidence # 1.0
addr.issues # [] (each Issue has .code, .message, .severity, .field)
addr.to_dict() # everything, JSON friendly
normalize_postal_code("e1n-1a1").code # "E1N 1A1"
normalize_province("PQ") # "QC"
parse_address("10 Oak Ave, Fredericton, NB, M5V 2T6").is_valid # FalseMore in examples/api_usage.py.
| Code | Severity | Meaning |
|---|---|---|
missing_postal_code |
error | No postal code |
invalid_postal_format |
error | Not shaped like A1A 1A1 |
invalid_postal_letter |
error | D F I O Q U anywhere, or W/Z first |
postal_province_mismatch |
error | First letter does not belong to the province |
unknown_province |
error | Province text not recognised |
missing_street, missing_street_name |
error | Nothing usable on the street line |
invalid_po_box, invalid_rural_route |
error | Box or route line without a number |
missing_province, missing_city |
warning | Field empty |
missing_civic_number |
warning | Street without a door number |
unknown_street_type |
warning | No street type recognised |
ambiguous_unit |
warning | Unit could be read two ways, or has no number |
multiple_delivery_lines, extra_address_line, unparsed_text |
warning | Something was left out of the label |
province_inferred |
info | Province taken from the postal code |
rural_fsa |
info | FSA with 0 as second character |
The confidence score starts at 1.0 and subtracts a fixed penalty per issue (see canada_address/issues.py). It is a rule-based score, not a probability.
Demo: examples/messy_addresses.csv holds 42 invented addresses (all 10 provinces and 3 territories, English and French, 14 of them deliberately flawed). Command and real output:
$ canada-address --csv examples/messy_addresses.csv -o examples/cleaned_addresses.csv
rows: 42 ok: 29 warnings: 3 errors: 10 mean confidence: 0.913
rural_fsa 3
invalid_postal_letter 3
postal_province_mismatch 2
ambiguous_unit 2
missing_postal_code 1
province_inferred 1
unknown_province 1
invalid_postal_format 1
missing_street 1
missing_civic_number 1
invalid_po_box 1
Excerpt of examples/cleaned_addresses.csv (label, confidence, issues):
123 MAIN ST, MIRAMICHI NB E1N 1A1 1.00 ok
4-55 WATER ST, ST JOHNS NL A1C 5X5 1.00 ok
300-45 BOUL SAINT LAURENT O, MONTREAL QC H2X 1Y1 1.00 ok
1502-22 BAY ST, TORONTO ON M5J 2T3 1.00 ok
PO BOX 1234 STN MAIN, WINNIPEG MB R3C 2E6 1.00 ok
SITE 5 COMP 7 RR 3, EXAMPLETON SK S0K 1A0 0.98 ok rural_fsa
CP 456 SUCC CENTRE VILLE, QUEBEC QC G1K 7A1 1.00 ok
5-17 LAKESHORE RD E, OAKVILLE ON L6J 1A1 1.00 ok
77 ELM ST, TORONTO ON D1A 1A1 0.65 error invalid_postal_letter
10 OAK AVE, FREDERICTON NB M5V 2T6 0.70 error postal_province_mismatch
15 CEDAR LANE, CALGARY AB T2P 1A1 0.97 ok province_inferred
9 BIRCH DR, OTTAWA K1A 0B1 0.75 error unknown_province
12-14 RUE NOTRE DAME, GATINEAU QC J8X 1A1 0.90 warning ambiguous_unit
PO BOX, THUNDER BAY ON P7B 1A1 0.70 error invalid_po_box
Tests: 232 passed in 0.78s (python -m pytest -q -p no:cacheprovider --import-mode=importlib canada-address from the workspace root).
Findings, honestly:
- Every one of the 14 flawed rows was flagged (10 as errors, 3 as warnings, 1 as an info issue), and every row I meant to be clean came out clean. That is expected, because I wrote both the data and the rules; it shows the rules do what they say, not how they fare on real exports.
- The tool cannot know whether a street or city exists. The demo postal codes are invented and only pass because the shape, letters and first-letter province are valid.
- Six rows are fine on the label but need a human eye:
12-14 Rue Notre-Dame(unit or civic range?),45 Main St Apt(unit number missing),Sunset Boulevard(no civic number), and the three rural FSAs, which are correct but rural. - Row 22 shows a policy choice: a missing province is filled from the postal code and reported as an info issue instead of an error.
- The demo file is ASCII-only, so accented input is covered by the unit tests rather than by the demo.
- Offline only: no check that a postal code, street or city actually exists.
- The street type table is a reasonable subset of Canada Post's abbreviations, written from the published conventions; check it against the official list if you need exact conformance.
- Free-text parsing expects commas or newlines between street, city, province and postal code. Without them, city text stays on the street line.
- Directions are recognised after the street type or name only (
Main St W), not before it. 12-14style numbers are read as unit-civic per Canada Post and flagged when they look like a civic range.- Not tested against a real customer database.
python -m pytest -q -p no:cacheprovider --import-mode=importlib canada-address
No network and no install needed; tests/conftest.py puts the tool folder on sys.path.
Marvin Palencia, founder of DeMark Studio, Miramichi, New Brunswick, Canada. Portfolio: marvin.demarkstudio.ca
MIT License.