Skip to content

About

Normalize and validate Canadian mailing addresses offline: postal codes vs province, EN/FR street types with Canada Post abbreviations, units, PO boxes and rural routes. CSV in, clean CSV out.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

canada-address

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.

What it does

  • 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. Avenue becomes AVE in English and AV in French; O (ouest) is used in French, W in English.
  • Units: Apt, Suite, Unit, #, app, appartement, bureau are moved to the Canada Post position (10-123 MAIN ST). Use --unit-style designator to keep the word (123 MAIN ST APT 10).
  • Civic numbers: 123A, 123 1/2, and 12-345 (unit 12 at civic 345).
  • Non-street delivery: PO Box, CP / case postale, RR (with SITE and COMP), General Delivery, Poste restante, with STN / SUCC stations.
  • 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.

Install

Python 3.10+. From a clone:

pip install .

or run without installing from the tool folder: python -m canada_address ....

CLI

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.

Exit 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.

Python API

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   # False

More in examples/api_usage.py.

Issue codes

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.

Measured result

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.

Limits

  • 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-14 style 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.

Tests

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.

About

Normalize and validate Canadian mailing addresses offline: postal codes vs province, EN/FR street types with Canada Post abbreviations, units, PO boxes and rural routes. CSV in, clean CSV out.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages