snailmailaddressparser
Version:
A Javascript library to parse snail mail addresses into components
301 lines (273 loc) • 9.26 kB
text/coffeescript
###
# CanadaStrategy
# --------------
# A way to parse Canadian addresses
#
# Reference: 'Addressing Guidelines'
# <http://www.canadapost.ca/tools/pg/manual/PGaddress-e.asp> 2012-11-01
#
###
#
#
class CanadaStrategy extends AddressStrategy
name: 'canada'
provinces_list = [
"AB", "Alberta",
"BC", "British\\s+Columbia",
"Manitoba", "MB",
"New Brunswick", "NB",
"Newfoundland\\s+and\\s+Labrador", "Newfoundland", "NF", "NL",
"Newfoundland\\s+&\\s+Labrador",
"Northwest Territories", "NT",
"Nova Scotia", "NS",
"Nunavut", "NU",
"ON", "Ontario",
"Prince\\s+Edward\\s+Island", "PE", "PEI",
"Quebec", "QC",
"Saskatchewan", "SK",
"Yukon", "YT",
]
# non-exhaustive list of street types
# (unused; my rexes took too long)
street_types = [
"Abbey", "ABBEY", "Acres", "ACRES", "Alley", "ALLEY", "Autoroute", "AUT",
"Avenue", "AVE", "Avenue", "AV", "Bay", "BAY", "Beach", "BEACH", "Bend",
"BEND", "Boulevard", "BLVD", "Boulevard", "BOUL", "building", "By-pass",
"BYPASS", "Byway", "BYWAY", "Campus", "CAMPUS", "Cape", "CAPE", "CAR",
"Carrefour", "CARREF", "Centre", "CTR", "Centre", "C", "Cercle", "CERCLE",
"Chase", "CHASE", "Chemin", "CH", "Circle", "CIR", "Circuit", "CIRCT",
"Close", "CLOSE", "Common", "COMMON", "Concession", "CONC", "Corners",
"CRNRS", "Cour", "COUR", "Cours", "COURS", "Court", "CRT", "Cove", "COVE",
"Crescent", "CRES", "Croissant", "CROIS", "Crossing", "CROSS",
"Cul-de-sac", "CDS", "Dale", "DALE", "Dell", "DELL", "Diversion", "DIVERS",
"Downs", "DOWNS", "Drive", "DR", "End", "END", "Esplanade", "ESPL",
"Estates", "ESTATE", "Expressway", "EXPY", "Extension", "EXTEN", "Farm",
"FARM", "Field", "FIELD", "Forest", "FOREST", "Freeway", "FWY", "Front",
"FRONT", "Gardens", "GDNS", "Gate", "GATE", "Glade", "GLADE", "Glen",
"GLEN", "Green", "GREEN", "Grounds", "GRNDS", "Grove", "GROVE", "Harbour",
"HARBR", "Heath", "HEATH", "Heights", "HTS", "Highlands", "HGHLDS",
"Highway", "HWY", "Hill", "HILL", "Hollow", "HOLLOW", "Impasse", "IMP",
"Inlet", "INLET", "Island", "ISLAND", "Key", "KEY", "Knoll", "KNOLL",
"Landing", "LANDNG", "Lane", "LANE", "Limits", "LMTS", "Line", "LINE",
"Link", "LINK", "Lookout", "LKOUT", "Loop", "LOOP", "Mall", "MALL",
"Manor", "MANOR", "Maze", "MAZE", "Meadow", "MEADOW", "Mews", "MEWS",
"Moor", "MOOR", "Mount", "MOUNT", "Mountain", "MTN", "Orchard", "ORCH",
"Parade", "PARADE", "Parc", "PARC", "Park", "PK", "Parkway", "PKY",
"Passage", "PASS", "Path", "PATH", "Pathway", "PTWAY", "Pines", "PINES",
"Place", "PL", "Place", "PLACE", "Plateau", "PLAT", "Plaza", "PLAZA",
"Point", "PT", "Pointe", "POINTE", "Port", "PORT", "Private", "PVT",
"Promenade", "PROM", "Quai", "QUAI", "Quay", "QUAY", "Ramp", "RAMP",
"Rang", "RANG", "Range", "RG", "Ridge", "RIDGE", "Rise", "RISE", "Road",
"RD", "Rond-point", "RDPT", "Route", "RTE", "Row", "ROW", "Rue", "RUE",
"Ruelle", "RLE", "Run", "RUN", "Sentier", "SENT", "Square", "SQ", "Street",
"ST", "Subdivision", "SUBDIV", "Terrace", "TERR", "Terrasse", "TSSE",
"Thicket", "THICK", "Towers", "TOWERS", "Townline", "TLINE", "Trail",
"TRAIL", "Turnabout", "TRNABT", "Vale", "VALE", "Via", "VIA", "View",
"VIEW", "Village", "VILLGE", "Villas", "VILLAS", "Vista", "VISTA", "Voie",
"VOIE", "Walk", "WALK", "Way", "WAY", "Wharf", "WHARF", "Wood", "WOOD",
"Wynd", "WYND",
]
person_rex = "(?: \\p{L} | [\\s\\-\\.])+ "
ADDRESSEE = new LineMatcher("Addressee",
"(?<addressee> #{person_rex})",
valid_tests: {
"Mary Swånson":
addressee: "Mary Swånson"
}, invalid_tests: [
"100 Sampsonite Drive"
]
)
CARE_OF = new LineMatcher "Care of (c/o)",
"(?: c/o | ℅) \\s+ (?<care_of> #{person_rex})",
valid_tests: {
"c/o Sinterklaas":
care_of: "Sinterklaas"
}, invalid_tests: [
"no c/o? fine. be that way."
]
street_rex = """
(?<street_number> \\d+) \\s+
(?<street_name> (?: \\p{L}|[\\.\\s\\-'])+? )
"""
PLAIN_STREET = new LineMatcher "Plain street",
street_rex,
valid_tests: {
"42 Wallaby Lane":
street_number: "42"
street_name: "Wallaby Lane"
"100 Hûntley Street":
street_number: "100"
street_name: "Hûntley Street"
}, invalid_tests: [
"Wallaby 12 Lane",
"Suite 100, 42 Wallaby Lane",
"42 Wallaby Lane, fl. 2-00",
]
unit = """
(?:
(?:
(?: apt\\.? | apartment | unit | suite | floor | fl\\.? | app | bureau )
\\s* (?: \\s [#] | no\\.? | number \s+ )?
| [#]
| no\\.
)
\\s*
[\\d\\w]+
|
[\\d\\w]+
\\s*
(?: floor | fl\\. )
)
"""
UNIT_STREET = new LineMatcher "Unit - Street",
"(?<suite> #{unit}|\\d+) \\s* [\\-,\\s] \\s* #{street_rex}",
valid_tests: {
"Suite 1100a - 42 Wallaby Ave.":
suite: "Suite 1100a"
street_number: "42"
street_name: "Wallaby Ave."
}, invalid_tests: [
"100 Wish Line, Unit #212",
"Any street with no unit"
]
STREET_UNIT = new LineMatcher "Street - Unit",
"#{street_rex} \\s* [,\\-]? \\s* (?<suite> #{unit})",
valid_tests: {
"42 Wallaby Ave., Suite 1100A":
suite: "Suite 1100A"
street_number: "42"
street_name: "Wallaby Ave."
"1 Rainy Road #115":
suite: "#115"
street_number: "1"
street_name: "Rainy Road"
# TODO / FIXME "Beothuk Building, 4th Floor":
#street_number: undefined
# street_name: "Beothuk Building"
# suite: "4th Floor"
}, invalid_tests: [
"100 100 100",
"Any street without a given unit"
]
PO_BOX = new LineMatcher "Post Office Box",
"""(?<po_box>P\\.?\\s* O\\.?\\s* BOX \\s* \\s* \\d+ \\s* ,? \\s*
(?: (?:stn\\.?|station|rpo\\.?|rr\\.?) \\s* \\w+)? )""",
valid_tests: {
"PO Box 1200":
po_box: "PO Box 1200"
"P.O. Box 1200 stn A":
po_box: "P.O. Box 1200 stn A"
"P.O. Box 39, RR1":
po_box: "P.O. Box 39, RR1"
}, invalid_tests: [
"No PO",
]
MORE_ADDR_INFO = new LineMatcher "Additional Address Anformation",
"(?<additional_address_info> .+)",
valid_tests: {
"Anything":
additional_address_info: "Anything"
}, invalid_tests: [
""
]
SUITE = new LineMatcher "Suite number",
"(?<suite> #{unit})",
valid_tests: {
'Suite # 1024':
suite: 'Suite # 1024'
}, invalid_tests: [
'10 10'
]
# XXX: Note that [\p{L}xyz] is *EXTRAORDINARILY* slow. Use (?:\p{L}|xyz)
MUNICIPALITY = new LineMatcher("Municipality and Province",
"(?<municipality> (?:\\p{L}|[\\-'\\s\\.])+?) \\s* ,? \\s*
(?<province> #{provinces_list.join("|")})",
valid_tests: {
"Bras-d'Or, NS":
municipality: "Bras-d'Or"
province: 'NS'
"St. Pétersberg, ON":
municipality: "St. Pétersberg"
province: "ON"
"Hudsonville, QC":
municipality: "Hudsonville"
province: "QC"
},
invalid_tests: [
"St. Peteresberg, Peterborough, 10005"
]
)
POSTAL = new LineMatcher("Postal code",
"(?<postal> \s*\\w\\d\\w\\s*\\d\\w\\d)",
valid_tests: {
"H0H0H0":
postal: 'H0H0H0'
"H0H 0H0":
postal: 'H0H 0H0'
},
invalid_tests: [
"HoH 0H0",
"HoH 0ü0" # (!unicode)
]
)
MUNICIPALITY_WITH_POSTAL = new LineMatcher(
"Municipality, province and postal code",
"#{MUNICIPALITY.expression} (?: \\s* ,? \\s* #{POSTAL.expression})?",
valid_tests: {
"One, QC, M5R 1V2":
municipality: "One"
province: "QC"
postal: "M5R 1V2"
"TreeTree, MB":
municipality: "TreeTree"
province: "MB"
postal: undefined
}, invalid_tests: [
"Two, Two, Two"
]
)
# Return an object with the fields we expect (and defaults)
expected_fields: ->
return {
suite: ''
addressee: ''
care_of: ''
street_number: ''
street_name: ''
additional_address_info: ''
po_box: ''
municipality: ''
province: ''
postal: ''
country: 'Canada'
}
#
# Return a list of line strategies that this country strategy employs
#
line_strategies: ->
lms = new LineMatcherStrategy()
street = PLAIN_STREET.or(UNIT_STREET).or(STREET_UNIT).or(PO_BOX).or(SUITE)
lms.add(ADDRESSEE.optional(),
CARE_OF.optional(),
street.optional(),
street,
MUNICIPALITY, POSTAL)
lms.add(ADDRESSEE.optional(),
CARE_OF.optional(),
street.optional(),
street,
MUNICIPALITY_WITH_POSTAL)
lms.add(ADDRESSEE,
MORE_ADDR_INFO.optional(),
CARE_OF.optional(),
street.optional(),
street,
MUNICIPALITY, POSTAL)
lms.add(ADDRESSEE,
MORE_ADDR_INFO.optional(),
CARE_OF.optional(),
street.optional(),
street,
MUNICIPALITY_WITH_POSTAL)
return lms.all()
new CanadaStrategy().register()