@mailwoman/codex 7.2.0 → 7.3.0

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.
Files changed (66) hide show
  1. package/address-system-conventions.ts +68 -0
  2. package/au/delivery-service.ts +179 -0
  3. package/au/index.ts +15 -0
  4. package/au/level-designator.ts +209 -0
  5. package/au/postcode.ts +51 -0
  6. package/au/state.ts +35 -0
  7. package/ca/index.ts +12 -0
  8. package/ca/postal-code.ts +121 -0
  9. package/ca/province.ts +99 -0
  10. package/ca/street-type.ts +167 -0
  11. package/country/codes.ts +534 -0
  12. package/country/country.ts +125 -0
  13. package/country/index.ts +15 -0
  14. package/country/names.ts +274 -0
  15. package/country/official-languages.ts +397 -0
  16. package/country/reference-data.ts +267 -0
  17. package/country/reference.ts +47 -0
  18. package/country/subdivision.ts +93 -0
  19. package/de/bundesland.ts +102 -0
  20. package/de/index.ts +12 -0
  21. package/de/postleitzahl.ts +91 -0
  22. package/de/street-type.ts +83 -0
  23. package/fr/cedex.ts +56 -0
  24. package/fr/code-postal.ts +105 -0
  25. package/fr/departement.ts +142 -0
  26. package/fr/index.ts +14 -0
  27. package/fr/region.ts +93 -0
  28. package/fr/voie.ts +98 -0
  29. package/gb/country.ts +74 -0
  30. package/gb/index.ts +14 -0
  31. package/gb/postcode-area.ts +107 -0
  32. package/gb/postcode.ts +109 -0
  33. package/gb/street-type.ts +90 -0
  34. package/index.ts +38 -0
  35. package/jp/address-unit.ts +87 -0
  36. package/jp/index.ts +13 -0
  37. package/jp/postal-code.ts +93 -0
  38. package/jp/prefecture.ts +173 -0
  39. package/level-semantics.ts +623 -0
  40. package/nz/delivery-service.ts +211 -0
  41. package/nz/index.ts +12 -0
  42. package/nz/postcode.ts +42 -0
  43. package/out/country/index.d.ts +1 -0
  44. package/out/country/index.d.ts.map +1 -1
  45. package/out/country/index.js +1 -0
  46. package/out/country/index.js.map +1 -1
  47. package/out/country/subdivision.d.ts +44 -0
  48. package/out/country/subdivision.d.ts.map +1 -0
  49. package/out/country/subdivision.js +76 -0
  50. package/out/country/subdivision.js.map +1 -0
  51. package/package.json +81 -37
  52. package/postcode-systems.ts +68 -0
  53. package/tools/build-country-surface-lexicon.ts +166 -0
  54. package/tools/export-country-surfaces.ts +46 -0
  55. package/tools/generate-country-reference.ts +153 -0
  56. package/tools/generate-official-languages.ts +188 -0
  57. package/tools/index.ts +12 -0
  58. package/us/floor-designator.ts +119 -0
  59. package/us/index.ts +19 -0
  60. package/us/military-address.ts +199 -0
  61. package/us/po-box.ts +82 -0
  62. package/us/state.ts +156 -0
  63. package/us/street-directional.ts +220 -0
  64. package/us/street-suffix.ts +345 -0
  65. package/us/unit-designator.ts +223 -0
  66. package/us/zipcode.ts +212 -0
package/gb/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The United Kingdom address system (Royal Mail / ISO 3166-2:GB): the variable-length alphanumeric
7
+ * postcode and its Royal-Mail postcode areas, the four constituent countries, and British street
8
+ * vocabulary.
9
+ */
10
+
11
+ export * from "./country.ts"
12
+ export * from "./postcode-area.ts"
13
+ export * from "./postcode.ts"
14
+ export * from "./street-type.ts"
@@ -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
+ }