@mailwoman/codex 7.1.0 → 7.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/address-system-conventions.ts +68 -0
- package/au/delivery-service.ts +179 -0
- package/au/index.ts +15 -0
- package/au/level-designator.ts +209 -0
- package/au/postcode.ts +51 -0
- package/au/state.ts +35 -0
- package/ca/index.ts +12 -0
- package/ca/postal-code.ts +121 -0
- package/ca/province.ts +99 -0
- package/ca/street-type.ts +167 -0
- package/country/codes.ts +534 -0
- package/country/country.ts +125 -0
- package/country/index.ts +14 -0
- package/country/names.ts +274 -0
- package/country/official-languages.ts +397 -0
- package/country/reference-data.ts +267 -0
- package/country/reference.ts +47 -0
- package/de/bundesland.ts +102 -0
- package/de/index.ts +12 -0
- package/de/postleitzahl.ts +91 -0
- package/de/street-type.ts +83 -0
- package/fr/cedex.ts +56 -0
- package/fr/code-postal.ts +105 -0
- package/fr/departement.ts +142 -0
- package/fr/index.ts +14 -0
- package/fr/region.ts +93 -0
- package/fr/voie.ts +98 -0
- package/gb/country.ts +74 -0
- package/gb/index.ts +14 -0
- package/gb/postcode-area.ts +107 -0
- package/gb/postcode.ts +109 -0
- package/gb/street-type.ts +90 -0
- package/index.ts +38 -0
- package/jp/address-unit.ts +87 -0
- package/jp/index.ts +13 -0
- package/jp/postal-code.ts +93 -0
- package/jp/prefecture.ts +173 -0
- package/level-semantics.ts +623 -0
- package/nz/delivery-service.ts +211 -0
- package/nz/index.ts +12 -0
- package/nz/postcode.ts +42 -0
- package/package.json +13 -5
- package/postcode-systems.ts +68 -0
- package/tools/build-country-surface-lexicon.ts +166 -0
- package/tools/export-country-surfaces.ts +46 -0
- package/tools/generate-country-reference.ts +153 -0
- package/tools/generate-official-languages.ts +188 -0
- package/tools/index.ts +12 -0
- package/us/floor-designator.ts +119 -0
- package/us/index.ts +19 -0
- package/us/military-address.ts +199 -0
- package/us/po-box.ts +82 -0
- package/us/state.ts +156 -0
- package/us/street-directional.ts +220 -0
- package/us/street-suffix.ts +345 -0
- package/us/unit-designator.ts +223 -0
- package/us/zipcode.ts +212 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Postcode AREA → constituent country, the Royal Mail mapping — and the concrete proof of the
|
|
7
|
+
* lesson in `postcode.ts` that UK postcodes do NOT track administrative geography.
|
|
8
|
+
*
|
|
9
|
+
* A postcode area is the leading one or two letters of a postcode (`SW`, `M`, `EH`, `BT`), named
|
|
10
|
+
* after the sorting town Royal Mail routes it through, NOT after a county or a constituent
|
|
11
|
+
* country. There is no clean postcode→admin hierarchy to inherit the way France gives you a
|
|
12
|
+
* département from the first two digits. The only honest thing we _can_ derive is which of the
|
|
13
|
+
* four UK countries an area predominantly falls in — and even that has border exceptions:
|
|
14
|
+
*
|
|
15
|
+
* - **TD** (Galashiels) and **SY** (Shrewsbury) straddle the Scotland/England and Wales/England
|
|
16
|
+
* borders respectively; each is assigned to its MAJORITY country here (TD → Scotland, SY →
|
|
17
|
+
* Wales). A handful of individual postcodes on the wrong side of the line are a gazetteer
|
|
18
|
+
* concern, not a thing this coarse table tries to model.
|
|
19
|
+
*
|
|
20
|
+
* So this is the ROYAL MAIL area→country mapping, and the fact that it needs a hand-built
|
|
21
|
+
* non-England set with documented border fudges — rather than a tidy prefix rule — is exactly why
|
|
22
|
+
* a UK postcode is not a county.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { UkCountryCode } from "./country.ts"
|
|
26
|
+
|
|
27
|
+
/** Northern Ireland is a single postcode area: BT (Belfast). */
|
|
28
|
+
const NORTHERN_IRELAND_AREAS = ["BT"] as const
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Scotland's postcode areas. TD (Galashiels) straddles the border with England and is assigned to Scotland as its
|
|
32
|
+
* majority country.
|
|
33
|
+
*/
|
|
34
|
+
const SCOTLAND_AREAS = [
|
|
35
|
+
"AB", // Aberdeen
|
|
36
|
+
"DD", // Dundee
|
|
37
|
+
"DG", // Dumfries
|
|
38
|
+
"EH", // Edinburgh
|
|
39
|
+
"FK", // Falkirk
|
|
40
|
+
"G", // Glasgow
|
|
41
|
+
"HS", // Outer Hebrides (Na h-Eileanan Siar)
|
|
42
|
+
"IV", // Inverness
|
|
43
|
+
"KA", // Kilmarnock
|
|
44
|
+
"KW", // Kirkwall (Orkney + Caithness)
|
|
45
|
+
"KY", // Kirkcaldy (Fife)
|
|
46
|
+
"ML", // Motherwell
|
|
47
|
+
"PA", // Paisley
|
|
48
|
+
"PH", // Perth
|
|
49
|
+
"TD", // Galashiels (Scottish Borders) — straddles the England border, majority Scotland
|
|
50
|
+
"ZE", // Lerwick (Shetland)
|
|
51
|
+
] as const
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Wales's postcode areas. SY (Shrewsbury) straddles the border with England and is assigned to Wales as its majority
|
|
55
|
+
* country.
|
|
56
|
+
*/
|
|
57
|
+
const WALES_AREAS = [
|
|
58
|
+
"CF", // Cardiff
|
|
59
|
+
"LD", // Llandrindod Wells
|
|
60
|
+
"LL", // Llandudno
|
|
61
|
+
"NP", // Newport
|
|
62
|
+
"SA", // Swansea
|
|
63
|
+
"SY", // Shrewsbury — straddles the Wales/England border, majority Wales
|
|
64
|
+
] as const
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The explicit non-England postcode areas, area → constituent country. England is intentionally absent: it is the
|
|
68
|
+
* DEFAULT (the great majority of UK areas are English), so listing it would be both enormous and a maintenance trap.
|
|
69
|
+
* Keeping only the non-England set makes the default transparent — anything not named here is England.
|
|
70
|
+
*/
|
|
71
|
+
export const GB_POSTCODE_AREA_COUNTRY: Record<string, UkCountryCode> = {
|
|
72
|
+
...Object.fromEntries(NORTHERN_IRELAND_AREAS.map((a) => [a, "NIR" as const])),
|
|
73
|
+
...Object.fromEntries(SCOTLAND_AREAS.map((a) => [a, "SCT" as const])),
|
|
74
|
+
...Object.fromEntries(WALES_AREAS.map((a) => [a, "WLS" as const])),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** True when `area` looks like a valid postcode-area string: one or two ASCII letters. */
|
|
78
|
+
function isAreaShape(area: unknown): area is string {
|
|
79
|
+
return typeof area === "string" && /^[A-Z]{1,2}$/i.test(area)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The constituent country a postcode AREA belongs to. Returns the explicit country for a known non-England area (e.g.
|
|
84
|
+
* `BT` → `NIR`, `G` → `SCT`, `CF` → `WLS`), and `ENG` as the default for any other validly-shaped area — England is by
|
|
85
|
+
* far the largest, so the default is transparent and the non-England exceptions live in
|
|
86
|
+
* {@link GB_POSTCODE_AREA_COUNTRY}. Returns null for clearly-invalid input (not one-or-two letters), so a malformed
|
|
87
|
+
* token is not silently called England.
|
|
88
|
+
*/
|
|
89
|
+
export function countryOfPostcodeArea(area: unknown): UkCountryCode | null {
|
|
90
|
+
if (!isAreaShape(area)) return null
|
|
91
|
+
|
|
92
|
+
return GB_POSTCODE_AREA_COUNTRY[area.toUpperCase()] ?? "ENG"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The constituent country a whole postcode resolves to, by extracting its area. `BT1 1AA` → `NIR`, `EH1 1BB` → `SCT`,
|
|
97
|
+
* `CF10 1AA` → `WLS`, `SW1A 1AA` → `ENG`. Null if the input has no extractable postcode area.
|
|
98
|
+
*/
|
|
99
|
+
export function countryOfPostcode(postcode: unknown): UkCountryCode | null {
|
|
100
|
+
if (typeof postcode !== "string") return null
|
|
101
|
+
// Extract the leading 1-2 letters directly, tolerating the inward code / spacing.
|
|
102
|
+
const match = /^\s*([A-Z]{1,2})/i.exec(postcode)
|
|
103
|
+
|
|
104
|
+
if (!match) return null
|
|
105
|
+
|
|
106
|
+
return countryOfPostcodeArea(match[1])
|
|
107
|
+
}
|
package/gb/postcode.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* UK postcodes: the branded type, the validation shape, normalization, and the outward/inward
|
|
7
|
+
* split.
|
|
8
|
+
*
|
|
9
|
+
* This is the MOST COMPLEX postcode of any system in the codex, and the contrast is the whole point
|
|
10
|
+
* of the file. A US ZIP, a German PLZ, and a French code postal are all a fixed five digits — the
|
|
11
|
+
* shape is trivial and the only interesting question is what admin unit the prefix maps to. The
|
|
12
|
+
* UK postcode is none of that:
|
|
13
|
+
*
|
|
14
|
+
* - It is **variable-length alphanumeric**, from six characters (`M1 1AE`) to eight (`SW1A 1AA`),
|
|
15
|
+
* across forms like `B33 8TH`, `CR2 6XH`, `DN55 1PT`.
|
|
16
|
+
* - It splits into an **OUTWARD code** (area + district, the part before the space — `SW1A`) and an
|
|
17
|
+
* **INWARD code** (sector + unit, the three chars after — `1AA`). Royal Mail sorts on the
|
|
18
|
+
* outward to a delivery office, then on the inward to a walk.
|
|
19
|
+
* - And — the lesson that propagates to `postcode-area.ts` — it does **NOT align with administrative
|
|
20
|
+
* geography**. A postcode area is a Royal Mail routing construct named after a sorting town
|
|
21
|
+
* (`SW` = south-west London, `EH` = Edinburgh), NOT a county or a constituent country. You
|
|
22
|
+
* cannot read a county off a UK postcode the way you read a département off a French one; the
|
|
23
|
+
* postcode→country mapping in `postcode-area.ts` exists precisely _because_ there is no clean
|
|
24
|
+
* hierarchy to inherit.
|
|
25
|
+
*
|
|
26
|
+
* So unlike the other systems, the hard work here is the shape itself — validating, normalizing the
|
|
27
|
+
* internal space, and cleaving outward from inward — not a prefix→admin lookup.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { Tagged } from "type-fest"
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A UK postcode: variable-length alphanumeric, outward + inward (`SW1A 1AA`, `M1 1AE`). The canonical form carries
|
|
34
|
+
* exactly one space before the final three characters. Unlike a US/DE/FR postcode, the shape is not a fixed-width
|
|
35
|
+
* numeric string — see {@link UK_POSTCODE_PATTERN}.
|
|
36
|
+
*
|
|
37
|
+
* @category Postal
|
|
38
|
+
* @type string
|
|
39
|
+
* @title UK postcode
|
|
40
|
+
* @pattern ^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$
|
|
41
|
+
*/
|
|
42
|
+
export type Postcode = Tagged<string, "UkPostcode">
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* UK postcode shape. A permissive form of the Royal Mail / UK-gov regex: one or two leading letters (the area), a
|
|
46
|
+
* district digit, an optional district letter-or-digit, then the inward sector digit and two unit letters, with the
|
|
47
|
+
* inward space optional so an un-spaced `SW1A1AA` still validates. The full UK-gov pattern additionally whitelists the
|
|
48
|
+
* British Overseas Territory codes (`ASCN`, `STHL`, `BBND`, …); those are rare enough to leave to the gazetteer.
|
|
49
|
+
*/
|
|
50
|
+
export const UK_POSTCODE_PATTERN = /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Normalize a UK postcode surface form: uppercase, strip surrounding whitespace, and ensure exactly one space before
|
|
54
|
+
* the final three characters (the inward code). `sw1a1aa` → `SW1A 1AA`, `M11AE` → `M1 1AE`, `b33 8th` → `B33 8TH`.
|
|
55
|
+
* Returns null if the result is not a valid postcode.
|
|
56
|
+
*/
|
|
57
|
+
export function normalizeUkPostcode(raw: unknown): Postcode | null {
|
|
58
|
+
if (typeof raw !== "string") return null
|
|
59
|
+
// Drop all whitespace, uppercase, then re-insert the single canonical space before the inward 3.
|
|
60
|
+
const compact = raw.replace(/\s+/g, "").toUpperCase()
|
|
61
|
+
|
|
62
|
+
if (compact.length < 5) return null
|
|
63
|
+
const spaced = `${compact.slice(0, -3)} ${compact.slice(-3)}`
|
|
64
|
+
|
|
65
|
+
return UK_POSTCODE_PATTERN.test(spaced) ? (spaced as Postcode) : null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Type-predicate for a UK postcode surface form (space optional). */
|
|
69
|
+
export function isUkPostcode(input: unknown): input is Postcode {
|
|
70
|
+
return typeof input === "string" && UK_POSTCODE_PATTERN.test(input.trim())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The OUTWARD code — the part before the space (area + district), e.g. `SW1A 1AA` → `SW1A`, `M1 1AE` → `M1`. Normalizes
|
|
75
|
+
* first so an un-spaced input still cleaves correctly; null if invalid.
|
|
76
|
+
*/
|
|
77
|
+
export function outwardCode(pc: unknown): string | null {
|
|
78
|
+
const normalized = normalizeUkPostcode(pc)
|
|
79
|
+
|
|
80
|
+
if (!normalized) return null
|
|
81
|
+
|
|
82
|
+
return normalized.slice(0, normalized.indexOf(" "))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The INWARD code — the three characters after the space (sector + unit), e.g. `SW1A 1AA` → `1AA`, `M1 1AE` → `1AE`.
|
|
87
|
+
* Null if invalid.
|
|
88
|
+
*/
|
|
89
|
+
export function inwardCode(pc: unknown): string | null {
|
|
90
|
+
const normalized = normalizeUkPostcode(pc)
|
|
91
|
+
|
|
92
|
+
if (!normalized) return null
|
|
93
|
+
|
|
94
|
+
return normalized.slice(normalized.indexOf(" ") + 1)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The POSTCODE AREA — the leading one or two LETTERS of the outward code, the Royal Mail routing region named after a
|
|
99
|
+
* sorting town: `SW1A 1AA` → `SW`, `M1 1AE` → `M`, `B33 8TH` → `B`. This is the key into `postcode-area.ts`'s
|
|
100
|
+
* area→country map. Null if the input is not a valid postcode.
|
|
101
|
+
*/
|
|
102
|
+
export function postcodeArea(pc: unknown): string | null {
|
|
103
|
+
const outward = outwardCode(pc)
|
|
104
|
+
|
|
105
|
+
if (!outward) return null
|
|
106
|
+
const match = /^[A-Z]{1,2}/.exec(outward)
|
|
107
|
+
|
|
108
|
+
return match ? match[0] : null
|
|
109
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* British street vocabulary (thoroughfare types).
|
|
7
|
+
*
|
|
8
|
+
* The contrast across the codex's street files is structural. A US street type is a trailing word
|
|
9
|
+
* with a USPS-standardized abbreviation (`Main Street` → `ST`). A German street type is a fused
|
|
10
|
+
* agglutinative suffix (`Hauptstraße`). A French type is a leading standalone word (`Rue de la
|
|
11
|
+
* Paix`). British practice is the US shape — a separate TRAILING word — but with a notably RICHER
|
|
12
|
+
* and more local vocabulary: alongside the everyday `Road`/`Street`/`Avenue` sit `Crescent`,
|
|
13
|
+
* `Mews`, `Close`, `Wynd`, `Brae`, `Gait`, `Croft`, `Dene` and a long tail of landscape words
|
|
14
|
+
* (`Brook`, `Copse`, `Dell`, `Hollow`, `Spinney`) that read as thoroughfare types in the UK and
|
|
15
|
+
* essentially nowhere else.
|
|
16
|
+
*
|
|
17
|
+
* So detection is, like French, a whole-token match — {@link isBritishStreetWord} asks "is this
|
|
18
|
+
* token a known British thoroughfare word" rather than testing a suffix — because these are
|
|
19
|
+
* distinct trailing words, not fused endings.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The British thoroughfare vocabulary. Lowercase canonical forms, matched as whole tokens. Spans the common core
|
|
24
|
+
* (`street`, `road`, `lane`, `avenue`) through the distinctively British (`crescent`, `mews`, `close`, `terrace`) and
|
|
25
|
+
* the regional/landscape tail (`wynd`, `brae`, `gait`, `croft`, `dene`, `spinney`, `dell`, `hollow`).
|
|
26
|
+
*/
|
|
27
|
+
export const GB_STREET_TYPES = [
|
|
28
|
+
"street",
|
|
29
|
+
"road",
|
|
30
|
+
"lane",
|
|
31
|
+
"avenue",
|
|
32
|
+
"close",
|
|
33
|
+
"crescent",
|
|
34
|
+
"court",
|
|
35
|
+
"drive",
|
|
36
|
+
"place",
|
|
37
|
+
"way",
|
|
38
|
+
"gardens",
|
|
39
|
+
"grove",
|
|
40
|
+
"terrace",
|
|
41
|
+
"mews",
|
|
42
|
+
"walk",
|
|
43
|
+
"hill",
|
|
44
|
+
"green",
|
|
45
|
+
"park",
|
|
46
|
+
"rise",
|
|
47
|
+
"view",
|
|
48
|
+
"row",
|
|
49
|
+
"square",
|
|
50
|
+
"parade",
|
|
51
|
+
"vale",
|
|
52
|
+
"wharf",
|
|
53
|
+
"yard",
|
|
54
|
+
"gate",
|
|
55
|
+
"croft",
|
|
56
|
+
"dene",
|
|
57
|
+
"end",
|
|
58
|
+
"fields",
|
|
59
|
+
"meadow",
|
|
60
|
+
"brook",
|
|
61
|
+
"chase",
|
|
62
|
+
"copse",
|
|
63
|
+
"dale",
|
|
64
|
+
"dell",
|
|
65
|
+
"glen",
|
|
66
|
+
"hollow",
|
|
67
|
+
"paddock",
|
|
68
|
+
"ridge",
|
|
69
|
+
"spinney",
|
|
70
|
+
"wynd",
|
|
71
|
+
"brae",
|
|
72
|
+
"gait",
|
|
73
|
+
] as const
|
|
74
|
+
|
|
75
|
+
/** A canonical British thoroughfare word (e.g. `street`, `crescent`, `mews`). */
|
|
76
|
+
export type BritishStreetType = (typeof GB_STREET_TYPES)[number]
|
|
77
|
+
|
|
78
|
+
const STREET_TYPE_SET: ReadonlySet<string> = new Set(GB_STREET_TYPES)
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* True when a token is a British thoroughfare type word (case-insensitive, whole-token match) — `Crescent`, `Mews`,
|
|
82
|
+
* `Close`, `Road`. Matches the WHOLE token, not a suffix, so an unrelated place name (`Tokyo`, `Bordeaux`) is not
|
|
83
|
+
* flagged the way an `-endsWith` test might.
|
|
84
|
+
*/
|
|
85
|
+
export function isBritishStreetWord(token: unknown): boolean {
|
|
86
|
+
if (typeof token !== "string") return false
|
|
87
|
+
const t = token.toLowerCase().replace(/[^a-z]/g, "")
|
|
88
|
+
|
|
89
|
+
return t.length > 0 && STREET_TYPE_SET.has(t)
|
|
90
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `@mailwoman/codex` — per-address-system postal reference data and branded types.
|
|
7
|
+
*
|
|
8
|
+
* Each address system (the USPS for the United States, La Poste for France, Deutsche Post for
|
|
9
|
+
* Germany, …) has its own conventions for what a postcode, a street suffix, or a unit designator
|
|
10
|
+
* looks like. This package is the shared, dependency-free home for that reference knowledge, kept
|
|
11
|
+
* apart from the locale-agnostic tokenizer/solver in `@mailwoman/core` and from the training
|
|
12
|
+
* pipeline in `@mailwoman/corpus`. The parser, the resolver, and the synthesis layer all reach
|
|
13
|
+
* for the same tables instead of each carrying their own copy.
|
|
14
|
+
*
|
|
15
|
+
* Systems are exposed as namespaces (`import { us } from "@mailwoman/codex"`) and as subpaths
|
|
16
|
+
* (`import { lookupStreetSuffix } from "@mailwoman/codex/us"`). The cross-system
|
|
17
|
+
* `candidateSystemsForPostcode` (the inverse of the per-slice postcode patterns) is a top-level
|
|
18
|
+
* export. `levels` is the per-locale LEVEL/floor ordinal-semantics table (#1100) — like
|
|
19
|
+
* `candidateSystemsForPostcode`, it's inherently multi-locale, so it lives at the codex root
|
|
20
|
+
* (`./level-semantics.ts`) and is namespaced rather than given its own `@mailwoman/codex/<x>`
|
|
21
|
+
* subpath.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
ADDRESS_SYSTEM_CONVENTIONS,
|
|
26
|
+
conventionsForSystem,
|
|
27
|
+
type AddressSystemConventions,
|
|
28
|
+
} from "./address-system-conventions.ts"
|
|
29
|
+
export * as au from "./au/index.ts"
|
|
30
|
+
export * as ca from "./ca/index.ts"
|
|
31
|
+
export * as de from "./de/index.ts"
|
|
32
|
+
export * as fr from "./fr/index.ts"
|
|
33
|
+
export * as gb from "./gb/index.ts"
|
|
34
|
+
export * as jp from "./jp/index.ts"
|
|
35
|
+
export * as levels from "./level-semantics.ts"
|
|
36
|
+
export * as nz from "./nz/index.ts"
|
|
37
|
+
export { candidateSystemsForPostcode, type SystemCode } from "./postcode-systems.ts"
|
|
38
|
+
export * as us from "./us/index.ts"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Japan's analog of a "street-type" module — except the lesson here is the absence. Where
|
|
7
|
+
* `us/street-suffix.ts` and `de/street-type.ts` exist to recognize the named-street part of an
|
|
8
|
+
* address, Japan has **no street names** to recognize. A Japanese address is built from nested
|
|
9
|
+
* administrative units and numbered blocks/lots, written largest-to-smallest:
|
|
10
|
+
*
|
|
11
|
+
* ```
|
|
12
|
+
* 東京都 千代田区 千代田 1丁目 1番 1号
|
|
13
|
+
* Tokyo-to · Chiyoda-ku · Chiyoda · 1-chōme · 1-ban · 1-gō
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* So instead of a street-suffix table this file ships two marker sets:
|
|
17
|
+
*
|
|
18
|
+
* - {@link JP_ADMIN_SUFFIXES} — the kanji that close an administrative-area name (都/道/府/県 at the
|
|
19
|
+
* prefecture level, then 市/区/郡/町/村 for city / ward / district / town / village). These are
|
|
20
|
+
* the Japanese equivalent of a US street suffix in the parsing sense: the token-final marker
|
|
21
|
+
* that tells you what KIND of unit the preceding name is.
|
|
22
|
+
* - {@link JP_BLOCK_MARKERS} — the markers that close the numbered tail (丁目 / 番地 / 番 / 号), the part
|
|
23
|
+
* that actually does the "house number" job in the absence of streets.
|
|
24
|
+
*
|
|
25
|
+
* Note the reverse field order: the admin suffixes appear FIRST in the string (prefecture leads),
|
|
26
|
+
* and the block markers LAST — the mirror image of a US line, where the house number leads and
|
|
27
|
+
* the ZIP trails. See `postal-code.ts` for why, with no street name, the postcode is the primary
|
|
28
|
+
* anchor.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The kanji suffixes that close an administrative-area name, largest unit to smallest:
|
|
33
|
+
*
|
|
34
|
+
* - 都 (to) — metropolis; only Tokyo.
|
|
35
|
+
* - 道 (dō) — circuit; only Hokkaido.
|
|
36
|
+
* - 府 (fu) — urban prefecture; Osaka and Kyoto.
|
|
37
|
+
* - 県 (ken) — prefecture; the other 43.
|
|
38
|
+
* - 市 (shi) — city.
|
|
39
|
+
* - 区 (ku) — ward (a subdivision of a designated city, e.g. Tokyo's 23 special wards).
|
|
40
|
+
* - 郡 (gun) — district / county (rural grouping of towns and villages).
|
|
41
|
+
* - 町 (chō / machi) — town.
|
|
42
|
+
* - 村 (son / mura) — village.
|
|
43
|
+
*/
|
|
44
|
+
export const JP_ADMIN_SUFFIXES = ["都", "道", "府", "県", "市", "区", "郡", "町", "村"] as const
|
|
45
|
+
|
|
46
|
+
/** A single administrative-area suffix kanji (`都`, `市`, `区`, …). */
|
|
47
|
+
export type JapaneseAdminSuffix = (typeof JP_ADMIN_SUFFIXES)[number]
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The markers that close the numbered tail of an address — Japan's stand-in for a house number, since there is no named
|
|
51
|
+
* street to hang one on:
|
|
52
|
+
*
|
|
53
|
+
* - 丁目 (chōme) — a district block within a neighbourhood.
|
|
54
|
+
* - 番地 (banchi) — a lot number.
|
|
55
|
+
* - 番 (ban) — block number (the `番` in the modern `chōme-ban-gō` triple).
|
|
56
|
+
* - 号 (gō) — building number (the final element of the triple).
|
|
57
|
+
*/
|
|
58
|
+
export const JP_BLOCK_MARKERS = ["丁目", "番地", "番", "号"] as const
|
|
59
|
+
|
|
60
|
+
/** A numbered-tail marker (`丁目`, `番地`, `番`, `号`). */
|
|
61
|
+
export type JapaneseBlockMarker = (typeof JP_BLOCK_MARKERS)[number]
|
|
62
|
+
|
|
63
|
+
const ADMIN_SUFFIX_SET: ReadonlySet<string> = new Set(JP_ADMIN_SUFFIXES)
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* True when a single kanji is one of the {@link JP_ADMIN_SUFFIXES} admin-area markers (`都`, `市`, `区`, …). Strictly
|
|
67
|
+
* single-character: a multi-character input (even one ending in a suffix) is not a suffix on its own.
|
|
68
|
+
*/
|
|
69
|
+
export function isJapaneseAdminSuffix(ch: unknown): ch is JapaneseAdminSuffix {
|
|
70
|
+
return typeof ch === "string" && ADMIN_SUFFIX_SET.has(ch)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Strip a trailing admin-area suffix kanji from a place name, exposing the bare name (`東京都` → `東京`, `大阪市` → `大阪`,
|
|
75
|
+
* `千代田区` → `千代田`). Leaves a name untouched if it does not end in an admin suffix.
|
|
76
|
+
*
|
|
77
|
+
* Hokkaido (`北海道`) is the deliberate exception: its name ends in 道 but is indivisible, so it is returned whole rather
|
|
78
|
+
* than clipped to `北海` — mirroring the same carve-out in `prefecture.ts`.
|
|
79
|
+
*/
|
|
80
|
+
export function stripAdminSuffix(name: string): string {
|
|
81
|
+
if (typeof name !== "string" || name.length === 0) return name
|
|
82
|
+
|
|
83
|
+
if (name === "北海道") return name
|
|
84
|
+
const last = name[name.length - 1]!
|
|
85
|
+
|
|
86
|
+
return ADMIN_SUFFIX_SET.has(last) ? name.slice(0, -1) : name
|
|
87
|
+
}
|
package/jp/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The Japanese address system (Japan Post / ISO 3166-2:JP): the 47 prefectures, the postal code
|
|
7
|
+
* that is the country's most reliable geographic anchor, and the address-unit markers that stand
|
|
8
|
+
* in for the street names Japan does not use.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export * from "./address-unit.ts"
|
|
12
|
+
export * from "./postal-code.ts"
|
|
13
|
+
export * from "./prefecture.ts"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Japanese postal codes (郵便番号, yūbin-bangō): the branded type, the shape, normalization, and the
|
|
7
|
+
* first-digit → coarse-region prior.
|
|
8
|
+
*
|
|
9
|
+
* This file is the far end of a spectrum whose other end is `us/zipcode.ts`. A US address leans on
|
|
10
|
+
* the street line — a named street plus a house number — and the ZIP is a routing convenience. A
|
|
11
|
+
* Japanese address is the inverse on two counts:
|
|
12
|
+
*
|
|
13
|
+
* - It is written **largest-to-smallest**: prefecture → city/ward → district → block → lot (`東京都 千代田区
|
|
14
|
+
* 千代田 1-1`), the reverse of the US smallest-to-largest line order.
|
|
15
|
+
* - There are **essentially no street names**. Outside a few Kyoto-style exceptions, you do not
|
|
16
|
+
* navigate by named streets; you navigate by nested administrative areas and numbered
|
|
17
|
+
* blocks/lots (丁目 / 番地 / 号 — see `address-unit.ts`).
|
|
18
|
+
*
|
|
19
|
+
* With no street name to anchor on and a reverse field order, the **postal code is the single most
|
|
20
|
+
* reliable geographic anchor** for a Japanese address — it pins the chōme-level area directly,
|
|
21
|
+
* far tighter than a US ZIP pins a US address. So where the US parser treats the postcode as a
|
|
22
|
+
* tie-breaker behind the street, the Japan parser should treat it as the primary key.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { Tagged } from "type-fest"
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A Japanese postal code: three digits, a hyphen, then four digits (`100-0001`), conventionally written after the 〒
|
|
29
|
+
* mark (`〒100-0001`). Branded so a normalized code is distinct from an arbitrary string — the 7-digit shape alone does
|
|
30
|
+
* not prove a code is real, only well-formed.
|
|
31
|
+
*
|
|
32
|
+
* @category Postal
|
|
33
|
+
* @type string
|
|
34
|
+
* @title 郵便番号
|
|
35
|
+
* @pattern ^\d{3}-?\d{4}$
|
|
36
|
+
*/
|
|
37
|
+
export type PostalCode = Tagged<string, "JpPostalCode">
|
|
38
|
+
|
|
39
|
+
/** The postal-code shape: `NNN-NNNN`, the hyphen optional on input (`1000001` or `100-0001`). */
|
|
40
|
+
export const JP_POSTAL_CODE_PATTERN = /^\d{3}-?\d{4}$/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalize a postal-code surface form to the canonical hyphenated `NNN-NNNN`: strip a leading 〒 mark and any
|
|
44
|
+
* whitespace, then re-insert the hyphen if the input gave the bare seven digits (`〒100-0001` → `100-0001`, `1000001` →
|
|
45
|
+
* `100-0001`). Returns null if the result is not seven digits.
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeJpPostalCode(raw: unknown): PostalCode | null {
|
|
48
|
+
if (typeof raw !== "string") return null
|
|
49
|
+
// Drop the 〒 mark and all whitespace, then keep only the digits.
|
|
50
|
+
const digits = raw.replace(/〒/g, "").replace(/\s+/g, "").replace(/-/g, "")
|
|
51
|
+
|
|
52
|
+
if (!/^\d{7}$/.test(digits)) return null
|
|
53
|
+
|
|
54
|
+
return `${digits.slice(0, 3)}-${digits.slice(3)}` as PostalCode
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Type-predicate for a Japanese postal code (hyphen optional, `100-0001` or `1000001`). */
|
|
58
|
+
export function isJpPostalCode(input: unknown): input is PostalCode {
|
|
59
|
+
return typeof input === "string" && JP_POSTAL_CODE_PATTERN.test(input)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* First digit of the postal code → a coarse region label. Japan Post's numbering grows roughly outward from Tokyo
|
|
64
|
+
* (`1xx`) and is **approximate** at this granularity — a single leading digit spans large, irregular areas and the
|
|
65
|
+
* boundaries are postal-routing, not administrative. Use it as a weak prior, never as a hard region assignment; the
|
|
66
|
+
* full code is what actually anchors the address.
|
|
67
|
+
*
|
|
68
|
+
* Approximate — the labels below are illustrative routing regions, not precise prefecture sets.
|
|
69
|
+
*/
|
|
70
|
+
export const JP_FIRST_DIGIT_REGION: Record<string, string> = {
|
|
71
|
+
"0": "Hokkaido & northern Tōhoku",
|
|
72
|
+
"1": "Tokyo & Kanto",
|
|
73
|
+
"2": "Kanagawa / Shizuoka & central",
|
|
74
|
+
"3": "northern Kanto / Tōhoku",
|
|
75
|
+
"4": "Tōkai / Chūbu",
|
|
76
|
+
"5": "Kinki / Kansai",
|
|
77
|
+
"6": "Kinki / Chūgoku west",
|
|
78
|
+
"7": "Chūgoku / Shikoku",
|
|
79
|
+
"8": "Kyūshū",
|
|
80
|
+
"9": "Tōhoku north / other",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The coarse region label for a postal code's first digit, or null if the input is not a Japanese postal code. A weak,
|
|
85
|
+
* approximate prior (see {@link JP_FIRST_DIGIT_REGION}); the full code anchors the address.
|
|
86
|
+
*/
|
|
87
|
+
export function firstDigitRegion(postalCode: unknown): string | null {
|
|
88
|
+
const normalized = normalizeJpPostalCode(postalCode)
|
|
89
|
+
|
|
90
|
+
if (!normalized) return null
|
|
91
|
+
|
|
92
|
+
return JP_FIRST_DIGIT_REGION[normalized[0]!] ?? null
|
|
93
|
+
}
|