@mailwoman/normalize 9.1.0 → 9.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 (52) hide show
  1. package/README.md +2 -2
  2. package/{abbreviations.ts → lib/abbreviations.ts} +3 -3
  3. package/{cjk.ts → lib/cjk.ts} +39 -4
  4. package/lib/comma-spacing.ts +68 -0
  5. package/{compute.ts → lib/compute.ts} +25 -9
  6. package/lib/fold.ts +30 -0
  7. package/{index.ts → lib/index.ts} +9 -8
  8. package/{nfc.ts → lib/nfc.ts} +1 -1
  9. package/{punctuation.ts → lib/punctuation.ts} +3 -1
  10. package/{types.ts → lib/types.ts} +8 -0
  11. package/{whitespace.ts → lib/whitespace.ts} +15 -11
  12. package/out/abbreviations.d.ts +2 -2
  13. package/out/abbreviations.d.ts.map +1 -1
  14. package/out/abbreviations.js +2 -2
  15. package/out/abbreviations.js.map +1 -1
  16. package/out/cjk.d.ts +7 -2
  17. package/out/cjk.d.ts.map +1 -1
  18. package/out/cjk.js +30 -4
  19. package/out/cjk.js.map +1 -1
  20. package/out/comma-spacing.d.ts +30 -0
  21. package/out/comma-spacing.d.ts.map +1 -0
  22. package/out/comma-spacing.js +52 -0
  23. package/out/comma-spacing.js.map +1 -0
  24. package/out/compute.d.ts +1 -1
  25. package/out/compute.d.ts.map +1 -1
  26. package/out/compute.js +22 -8
  27. package/out/compute.js.map +1 -1
  28. package/out/fold.d.ts +22 -0
  29. package/out/fold.d.ts.map +1 -0
  30. package/out/fold.js +28 -0
  31. package/out/fold.js.map +1 -0
  32. package/out/index.d.ts +9 -8
  33. package/out/index.d.ts.map +1 -1
  34. package/out/index.js +8 -7
  35. package/out/index.js.map +1 -1
  36. package/out/nfc.d.ts.map +1 -1
  37. package/out/nfc.js +1 -1
  38. package/out/nfc.js.map +1 -1
  39. package/out/offset-map.d.ts.map +1 -1
  40. package/out/offset-map.js.map +1 -1
  41. package/out/punctuation.d.ts.map +1 -1
  42. package/out/punctuation.js +3 -1
  43. package/out/punctuation.js.map +1 -1
  44. package/out/types.d.ts +10 -0
  45. package/out/types.d.ts.map +1 -1
  46. package/out/types.js.map +1 -1
  47. package/out/whitespace.d.ts +9 -3
  48. package/out/whitespace.d.ts.map +1 -1
  49. package/out/whitespace.js +11 -10
  50. package/out/whitespace.js.map +1 -1
  51. package/package.json +111 -3
  52. /package/{offset-map.ts → lib/offset-map.ts} +0 -0
package/README.md CHANGED
@@ -46,7 +46,7 @@ identityMap(length: number): OffsetMap
46
46
  ## Pipeline position
47
47
 
48
48
  ```
49
- raw string → normalize → query-shape → locale-gate → kind-classifier → phrase-grouper → ...
49
+ raw string → normalize → query-shape → locale-hint → kind-classifier → phrase-grouper → ...
50
50
  ```
51
51
 
52
52
  Stage 1 in the [Staged Pipeline Contract](https://github.com/sister-software/mailwoman/blob/main/docs/engineering/reference/STAGES.mdx). No runtime dependencies.
@@ -61,7 +61,7 @@ Stage 1 in the [Staged Pipeline Contract](https://github.com/sister-software/mai
61
61
 
62
62
  - [`@mailwoman/query-shape`](../query-shape) — Stage 1.5, structural priors that consume the normalized output
63
63
  - [Staged Pipeline Contract](https://github.com/sister-software/mailwoman/blob/main/docs/engineering/reference/STAGES.mdx)
64
- - [Tokenization concepts](https://mailwoman.sister.software/articles/concepts/tokenization/)
64
+ - [Tokenization concepts](https://mailwoman.ai/articles/concepts/tokenization/)
65
65
 
66
66
  ## License
67
67
 
@@ -12,7 +12,7 @@
12
12
  * intentionally — refactoring sharing is a separate task.
13
13
  */
14
14
 
15
- import type { SpanRange } from "./types.ts"
15
+ import type { SpanRange } from "#types"
16
16
 
17
17
  export interface AbbreviationEntry {
18
18
  from: string // short form (case-insensitive match)
@@ -80,7 +80,7 @@ const ES_ES_DICT: ReadonlyArray<AbbreviationEntry> = [
80
80
  * Dropping the entry is NOT a table edit: `fr-op3-halles-market-bonneuil` is a passing row that asserts street "Avenue
81
81
  * de la Convention" and an `address_point` tier, so it pins the current behaviour and a removal has to be measured on a
82
82
  * resolver-gauntlet run. The real repair is upstream — the geocode path hardcodes `locale: "und"` because Stage 1
83
- * precedes the parse, and `@mailwoman/locale-gate` cannot presently detect Spanish (it scores script class + known
83
+ * precedes the parse, and `@mailwoman/locale-hint` cannot presently detect Spanish (it scores script class + known
84
84
  * postcode formats, and a 5-digit ES/MX code is indistinguishable from a US ZIP).
85
85
  */
86
86
  const LOCALE_UNKNOWN_DICT: ReadonlyArray<AbbreviationEntry> = [
@@ -112,7 +112,7 @@ function getDictionary(locale: string | undefined): ReadonlyArray<AbbreviationEn
112
112
  /**
113
113
  * The per-locale abbreviation table (short↔long), exposed so consumers can reuse the SAME data instead of duplicating
114
114
  * it. The metamorphic gauntlet inverts this table to generate expanded→abbreviated perturbations (`Avenue`→`Ave`); the
115
- * "no load-bearing trivia" rule means that data lives in exactly one place — here.
115
+ * "no required trivia" rule means that data lives in exactly one place — here.
116
116
  */
117
117
  export function abbreviationDictionary(locale?: string): ReadonlyArray<AbbreviationEntry> {
118
118
  return getDictionary(locale)
@@ -16,6 +16,9 @@
16
16
  * 1, a full-width `-` always a hyphen — keyboards and copy-paste produce these constantly.
17
17
  * Folding them to ASCII makes `104−0061` and `104-0061` the same input.
18
18
  * - **Fold the ideographic space (U+3000 → ' ').**
19
+ * - **Fold half-width katakana (U+FF61–U+FF9F).** This matches the JP corpus builder. A voiced
20
+ * pair such as `デ` contracts from two UTF-16 units to one `デ`; the output offset maps to the
21
+ * first raw unit in the pair.
19
22
  *
20
23
  * It deliberately does NOT convert **kanji numerals** (一二三…): place names carry numeral kanji as
21
24
  * ordinary characters (三田 _Mita_, 四谷 _Yotsuya_), so a blind 三→3 would corrupt them.
@@ -23,11 +26,11 @@
23
26
  * normalization — deferred. Kana→kanji transliteration (ちょうめ→丁目) is dictionary work and likewise
24
27
  * deferred.
25
28
  *
26
- * Self-gating: a string with none of these characters returns identity, so Latin input is
29
+ * Self-limiting: a string with none of these characters returns identity, so Latin input is
27
30
  * untouched.
28
31
  */
29
32
 
30
- import { identityMap } from "./offset-map.ts"
33
+ import { identityMap } from "#offset-map"
31
34
 
32
35
  export interface CjkResult {
33
36
  text: string
@@ -59,19 +62,27 @@ const IDEOGRAPHIC_SPACE = 0x30_00
59
62
  * 〒.
60
63
  */
61
64
  const POSTAL_MARK = 0x30_12
65
+ const HALFWIDTH_KATAKANA_START = 0xff_61
66
+ const HALFWIDTH_KATAKANA_END = 0xff_9f
67
+ const HALFWIDTH_VOICING_START = 0xff_9e
62
68
 
63
- export function applyCjkNormalization(input: string): CjkResult {
69
+ function isHalfwidthKatakana(code: number): boolean {
70
+ return code >= HALFWIDTH_KATAKANA_START && code <= HALFWIDTH_KATAKANA_END
71
+ }
72
+
73
+ export function applyCjkNormalization(input: string, opts: { postalMark?: "strip" | "keep" } = {}): CjkResult {
64
74
  let folded = 0
65
75
  let stripped = 0
66
76
  const out: string[] = []
67
77
  const map: number[] = []
78
+ const stripPostalMark = opts.postalMark !== "keep"
68
79
 
69
80
  // All transformed code points are in the BMP (single UTF-16 unit), and every other character is
70
81
  // passed through verbatim, so a per-unit walk is safe for surrogate-pair input too.
71
82
  for (let i = 0; i < input.length; i++) {
72
83
  const code = input.charCodeAt(i)
73
84
 
74
- if (code === POSTAL_MARK) {
85
+ if (code === POSTAL_MARK && stripPostalMark) {
75
86
  stripped += 1
76
87
 
77
88
  continue // drop — no addressing content; whitespace collapse later tidies any gap
@@ -93,6 +104,30 @@ export function applyCjkNormalization(input: string): CjkResult {
93
104
  continue
94
105
  }
95
106
 
107
+ if (isHalfwidthKatakana(code)) {
108
+ const next = input.charCodeAt(i + 1)
109
+
110
+ const consumesVoicingMark =
111
+ code < HALFWIDTH_VOICING_START && next >= HALFWIDTH_VOICING_START && next <= HALFWIDTH_KATAKANA_END
112
+
113
+ const width = consumesVoicingMark ? 2 : 1
114
+
115
+ const normalized = input
116
+ .slice(i, i + width)
117
+ .normalize("NFKC")
118
+ .normalize("NFC")
119
+
120
+ for (const unit of normalized) {
121
+ out.push(unit)
122
+ map.push(i)
123
+ }
124
+
125
+ folded += width
126
+ i += width - 1
127
+
128
+ continue
129
+ }
130
+
96
131
  out.push(input[i]!)
97
132
  map.push(i)
98
133
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Comma spacing — a comma directly followed by a letter gains one space, so `Biggin Hill,United Kingdom`
7
+ * reaches stage 2 as `Biggin Hill, United Kingdom`. A tight comma is an ordinary typing pattern, and the parse
8
+ * is the first stage that reads it differently: with no space, the tokenizer glues `,United` into one piece,
9
+ * the segmenter still splits, and the decoder labels the glued piece as a street or a locality it is not.
10
+ *
11
+ * A letter or a digit after the comma triggers the insertion, with one exception: a comma with a digit on
12
+ * BOTH sides is a numeric separator (`12,5`, `1,000`) and is left as typed. `Köln,50733` has a letter before
13
+ * the comma, so it is a list separator and gains the space. A space, punctuation or end of input after the
14
+ * comma is left as typed. The inserted space maps to the comma's own offset, the same rule the `…`
15
+ * expansion in `punctuation.ts` follows, so every span that starts after it still points into the raw input.
16
+ */
17
+
18
+ import { identityMap } from "#offset-map"
19
+
20
+ /**
21
+ * Any letter or decimal digit in any script: the Unicode property classes `\p{L}` and `\p{Nd}`.
22
+ */
23
+ const LETTER_OR_DIGIT = /[\p{L}\p{Nd}]/u
24
+ const DIGIT = /\p{Nd}/u
25
+
26
+ export interface CommaSpacingResult {
27
+ text: string
28
+ map: number[]
29
+ /**
30
+ * How many spaces were inserted.
31
+ */
32
+ inserted: number
33
+ }
34
+
35
+ /**
36
+ * A comma with a decimal digit on both sides: `12,5`, `1,000`.
37
+ */
38
+ function isNumericSeparator(input: string, commaIndex: number): boolean {
39
+ return commaIndex > 0 && DIGIT.test(input[commaIndex - 1]!) && DIGIT.test(input[commaIndex + 1]!)
40
+ }
41
+
42
+ /**
43
+ * Insert one space after every comma that is directly followed by a letter or digit, unless the comma is a numeric
44
+ * separator. Offset-map-correct: the inserted space maps to the comma.
45
+ */
46
+ export function spaceAfterComma(input: string): CommaSpacingResult {
47
+ let inserted = 0
48
+ const out: string[] = []
49
+ const map: number[] = []
50
+
51
+ for (let i = 0; i < input.length; i++) {
52
+ const ch = input[i]!
53
+ out.push(ch)
54
+ map.push(i)
55
+
56
+ if (ch === "," && i + 1 < input.length && LETTER_OR_DIGIT.test(input[i + 1]!) && !isNumericSeparator(input, i)) {
57
+ out.push(" ")
58
+ map.push(i)
59
+ inserted += 1
60
+ }
61
+ }
62
+
63
+ if (!inserted) {
64
+ return { text: input, map: identityMap(input.length), inserted: 0 }
65
+ }
66
+
67
+ return { text: out.join(""), map, inserted }
68
+ }
@@ -7,13 +7,14 @@
7
7
  * (always) with case-fold + abbreviation expansion (opt-in).
8
8
  */
9
9
 
10
- import { expandAbbreviations } from "./abbreviations.ts"
11
- import { applyCjkNormalization } from "./cjk.ts"
12
- import { applyNFC } from "./nfc.ts"
13
- import { composeMaps, identityMap } from "./offset-map.ts"
14
- import { applyPunctuation } from "./punctuation.ts"
15
- import type { NormalizationTransform, NormalizedInput, NormalizeOpts } from "./types.ts"
16
- import { collapseWhitespace } from "./whitespace.ts"
10
+ import { expandAbbreviations } from "#abbreviations"
11
+ import { applyCjkNormalization } from "#cjk"
12
+ import { spaceAfterComma } from "#comma-spacing"
13
+ import { applyNFC } from "#nfc"
14
+ import { composeMaps, identityMap } from "#offset-map"
15
+ import { applyPunctuation } from "#punctuation"
16
+ import type { NormalizationTransform, NormalizedInput, NormalizeOpts } from "#types"
17
+ import { collapseWhitespace } from "#whitespace"
17
18
 
18
19
  export function normalize(raw: string, opts?: NormalizeOpts): NormalizedInput {
19
20
  const transforms: NormalizationTransform[] = []
@@ -32,7 +33,7 @@ export function normalize(raw: string, opts?: NormalizeOpts): NormalizedInput {
32
33
  // parse) and fold full-width ASCII + the ideographic space. Runs after NFC so it sees composed
33
34
  // forms, before punctuation/whitespace so any gap left by 〒 is then collapsed. No-op off-script.
34
35
  {
35
- const r = applyCjkNormalization(text)
36
+ const r = applyCjkNormalization(text, opts?.postalMark ? { postalMark: opts.postalMark } : {})
36
37
 
37
38
  if (r.folded > 0 || r.stripped > 0) {
38
39
  text = r.text
@@ -52,11 +53,26 @@ export function normalize(raw: string, opts?: NormalizeOpts): NormalizedInput {
52
53
  }
53
54
  }
54
55
 
56
+ // 2.5 Comma spacing — a comma glued to a letter gains a space. Runs after punctuation (so a folded
57
+ // full-width comma is seen) and before whitespace collapse (so a comma already followed by a space is
58
+ // never doubled).
59
+ {
60
+ const r = spaceAfterComma(text)
61
+
62
+ if (r.inserted > 0) {
63
+ text = r.text
64
+ map = composeMaps(map, r.map)
65
+ transforms.push({ kind: "space_after_comma", inserted: r.inserted })
66
+ }
67
+ }
68
+
55
69
  // 3. Whitespace
56
70
  {
57
71
  const r = collapseWhitespace(text)
58
72
 
59
- if (r.runs > 0 || r.text.length !== text.length) {
73
+ // Compare the TEXT, not its length: folding a lone tab to a space is length-preserving, and a
74
+ // length test reads that edit as no edit at all.
75
+ if (r.text !== text) {
60
76
  text = r.text
61
77
  map = composeMaps(map, r.map)
62
78
  transforms.push({ kind: "collapse_whitespace", runs: r.runs })
package/lib/fold.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ * @file Match-key folds: the loss-y transforms a comparison applies to both sides.
6
+ */
7
+
8
+ /**
9
+ * Lower-case, whitespace runs collapsed to one space, ends trimmed. The fold every string comparison applies before it
10
+ * compares; it keeps punctuation and diacritics, so `Saint-Étienne` and `Saint-Etienne` stay distinct.
11
+ */
12
+ export function foldCaseWhitespace(input: string): string {
13
+ return input.toLowerCase().replaceAll(/\s+/gu, " ").trim()
14
+ }
15
+
16
+ /**
17
+ * Combining marks removed after NFD decomposition: `é` → `e`, `ł` unchanged (it is not a base plus a mark). Case and
18
+ * whitespace are untouched.
19
+ */
20
+ export function stripCombiningMarks(input: string): string {
21
+ return input.normalize("NFD").replaceAll(/\p{M}/gu, "")
22
+ }
23
+
24
+ /**
25
+ * {@link foldCaseWhitespace} over the NFKC composition, with locale-aware lower-casing: fullwidth and compatibility
26
+ * forms fold together before comparison.
27
+ */
28
+ export function foldNFKCWhitespace(input: string): string {
29
+ return input.normalize("NFKC").toLocaleLowerCase().replaceAll(/\s+/gu, " ").trim()
30
+ }
@@ -13,11 +13,12 @@
13
13
  * See `docs/engineering/reference/STAGES.md` § Stage 1 for the contract.
14
14
  */
15
15
 
16
- export { type AbbreviationEntry, abbreviationDictionary, expandAbbreviations } from "./abbreviations.ts"
17
- export { applyCjkNormalization, type CjkResult } from "./cjk.ts"
18
- export { normalize } from "./compute.ts"
19
- export { applyNFC } from "./nfc.ts"
20
- export { composeMaps, identityMap } from "./offset-map.ts"
21
- export { applyPunctuation } from "./punctuation.ts"
22
- export type { NormalizationTransform, NormalizeOpts, NormalizedInput, SpanRange } from "./types.ts"
23
- export { collapseWhitespace } from "./whitespace.ts"
16
+ export { type AbbreviationEntry, abbreviationDictionary, expandAbbreviations } from "#abbreviations"
17
+ export { applyCjkNormalization, type CjkResult } from "#cjk"
18
+ export { normalize } from "#compute"
19
+ export { foldCaseWhitespace, foldNFKCWhitespace, stripCombiningMarks } from "#fold"
20
+ export { applyNFC } from "#nfc"
21
+ export { composeMaps, identityMap } from "#offset-map"
22
+ export { applyPunctuation } from "#punctuation"
23
+ export type { NormalizationTransform, NormalizeOpts, NormalizedInput, SpanRange } from "#types"
24
+ export { collapseWhitespace } from "#whitespace"
@@ -12,7 +12,7 @@
12
12
  * variant selectors may produce off-by-one offsets — acceptable for v1.
13
13
  */
14
14
 
15
- import { identityMap } from "./offset-map.ts"
15
+ import { identityMap } from "#offset-map"
16
16
 
17
17
  export interface NFCResult {
18
18
  text: string
@@ -7,7 +7,7 @@
7
7
  * fancy character is a single codepoint that maps to a single ASCII char.
8
8
  */
9
9
 
10
- import { identityMap } from "./offset-map.ts"
10
+ import { identityMap } from "#offset-map"
11
11
 
12
12
  const REPLACEMENTS = new Map<string, string>([
13
13
  ["‘", "'"], // ‘
@@ -16,8 +16,10 @@ const REPLACEMENTS = new Map<string, string>([
16
16
  ["”", '"'], // ”
17
17
  ["–", "-"], // – en dash
18
18
  ["—", "-"], // — em dash
19
+ ["‐", "-"], // ‐ U+2010 hyphen
19
20
  ["−", "-"], // − U+2212 minus sign — Japanese IMEs emit this as the block separator (1−2−3)
20
21
  ["―", "-"], // ― U+2015 horizontal bar — another common JP block separator
22
+ ["﹣", "-"], // ﹣ U+FE63 small hyphen-minus
21
23
  ["…", "..."], // … expands; tracked specially
22
24
  [" ", " "], // non-breaking space
23
25
  ])
@@ -19,6 +19,7 @@ export type NormalizationTransform =
19
19
  | { kind: "expand_abbreviation"; from: string; to: string; at: SpanRange }
20
20
  | { kind: "collapse_whitespace"; runs: number }
21
21
  | { kind: "normalize_punctuation"; replacements: number }
22
+ | { kind: "space_after_comma"; inserted: number }
22
23
  | { kind: "normalize_cjk"; folded: number; stripped: number }
23
24
 
24
25
  /**
@@ -75,4 +76,11 @@ export interface NormalizeOpts {
75
76
  * Skip Unicode NFC. Only use for debugging — production callers should leave on.
76
77
  */
77
78
  skipNFC?: boolean
79
+ /**
80
+ * What the CJK pass does with the postal mark 〒 (U+3012). `strip` (the default) drops it: it is byte-fallback OOV for
81
+ * the SentencePiece tokenizer and fragments the digits after it. `keep` leaves it in place for a classifier whose
82
+ * vocabulary carries it — the character-path CJK model was trained with the mark in front of every postcode, and
83
+ * without it misreads the prefecture boundary (`885-0061 宮崎県都城市…` → prefecture `崎県都`).
84
+ */
85
+ postalMark?: "strip" | "keep"
78
86
  }
@@ -3,28 +3,34 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * Whitespace collapse — runs of whitespace become a single ASCII space. Newlines and tabs are
7
- * preserved as-is (segmentation grammar in QueryShape uses them); inline runs of spaces
8
- * collapse. The trailing trim also drops trailing sentence-punctuation NOISE (#829 tail): a
6
+ * Whitespace collapse — a run of inline whitespace (`[ \t]`) becomes one ASCII space AT EVERY RUN
7
+ * LENGTH, so a lone tab normalizes exactly as a doubled one does. Newlines (`\n`/`\r`) are preserved:
8
+ * QueryShape's segmentation grammar reads one as a segment separator on a par with a comma, and it
9
+ * reads a RAW tab the same way — folding every tab here is what keeps a tab-separated export from
10
+ * re-segmenting the query. The trailing trim also drops trailing sentence-punctuation NOISE (#829 tail): a
9
11
  * trailing `.`/`,`/`;`/`:` (e.g. `…Washington DC.`) glues onto the last token and drops the street
10
12
  * tier (`address_point`→`admin`). Trailing only + a conservative set — leading punctuation and
11
13
  * quotes/brackets are never touched (they can be meaningful). Offset-map-correct via the same slice
12
14
  * as the whitespace trim, so span alignment survives.
13
15
  */
14
16
 
15
- import { identityMap } from "./offset-map.ts"
17
+ import { identityMap } from "#offset-map"
16
18
 
17
19
  const INLINE_SPACE = /[ \t]/
18
20
  const ANY_SPACE = /[ \t\n\r]/
19
21
  /**
20
22
  * Trailing NOISE trimmed off the END of the input: whitespace + the sentence-punctuation that a user commonly appends.
21
- * NOT leading (a leading token is load-bearing) and NOT quotes/brackets/parens.
23
+ * NOT leading (a leading token is required) and NOT quotes/brackets/parens.
22
24
  */
23
25
  const TRAILING_NOISE = /[ \t\n\r.,;:]/
24
26
 
25
27
  export interface WhitespaceResult {
26
28
  text: string
27
29
  map: number[]
30
+ /**
31
+ * How many inline-whitespace runs were REWRITTEN — a run longer than one character, or a one-character run that was
32
+ * not already an ASCII space. A run that was already a single space is not one of them.
33
+ */
28
34
  runs: number
29
35
  }
30
36
 
@@ -57,7 +63,10 @@ export function collapseWhitespace(input: string): WhitespaceResult {
57
63
  i += 1
58
64
  }
59
65
 
60
- if (i - start > 1) {
66
+ // A one-character run counts too when the character is not already an ASCII space: the tab→space
67
+ // rewrite emitted above is a real edit, and `changed` is what decides whether the caller ever
68
+ // receives it — the early return below hands back the untouched input otherwise.
69
+ if (i - start > 1 || ch !== " ") {
61
70
  changed = true
62
71
  runs += 1
63
72
  }
@@ -65,11 +74,6 @@ export function collapseWhitespace(input: string): WhitespaceResult {
65
74
  continue
66
75
  }
67
76
 
68
- // Collapse \r\n into one
69
- if (ch === "\n" && out.at(-1) === "\r") {
70
- // Already handled in CR branch above by emitting both; skip combiner check
71
- }
72
-
73
77
  out.push(ch)
74
78
  map.push(i)
75
79
  i += 1
@@ -11,7 +11,7 @@
11
11
  * augmentation). Both sides should eventually share dictionaries; for v1 this dict is duplicated
12
12
  * intentionally — refactoring sharing is a separate task.
13
13
  */
14
- import type { SpanRange } from "./types.ts";
14
+ import type { SpanRange } from "#types";
15
15
  export interface AbbreviationEntry {
16
16
  from: string;
17
17
  to: string;
@@ -19,7 +19,7 @@ export interface AbbreviationEntry {
19
19
  /**
20
20
  * The per-locale abbreviation table (short↔long), exposed so consumers can reuse the SAME data instead of duplicating
21
21
  * it. The metamorphic gauntlet inverts this table to generate expanded→abbreviated perturbations (`Avenue`→`Ave`); the
22
- * "no load-bearing trivia" rule means that data lives in exactly one place — here.
22
+ * "no required trivia" rule means that data lives in exactly one place — here.
23
23
  */
24
24
  export declare function abbreviationDictionary(locale?: string): ReadonlyArray<AbbreviationEntry>;
25
25
  export interface AbbreviationResult {
@@ -1 +1 @@
1
- {"version":3,"file":"abbreviations.d.ts","sourceRoot":"","sources":["../abbreviations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE3C,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;CACV;AA4FD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAExF;AAED,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,EAAE,CAAA;IACb,UAAU,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;CAC9D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAmEtF"}
1
+ {"version":3,"file":"abbreviations.d.ts","sourceRoot":"","sources":["../lib/abbreviations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAEvC,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;CACV;AA4FD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAExF;AAED,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,EAAE,CAAA;IACb,UAAU,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;CAC9D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAmEtF"}
@@ -69,7 +69,7 @@ const ES_ES_DICT = [
69
69
  * Dropping the entry is NOT a table edit: `fr-op3-halles-market-bonneuil` is a passing row that asserts street "Avenue
70
70
  * de la Convention" and an `address_point` tier, so it pins the current behaviour and a removal has to be measured on a
71
71
  * resolver-gauntlet run. The real repair is upstream — the geocode path hardcodes `locale: "und"` because Stage 1
72
- * precedes the parse, and `@mailwoman/locale-gate` cannot presently detect Spanish (it scores script class + known
72
+ * precedes the parse, and `@mailwoman/locale-hint` cannot presently detect Spanish (it scores script class + known
73
73
  * postcode formats, and a 5-digit ES/MX code is indistinguishable from a US ZIP).
74
74
  */
75
75
  const LOCALE_UNKNOWN_DICT = [
@@ -98,7 +98,7 @@ function getDictionary(locale) {
98
98
  /**
99
99
  * The per-locale abbreviation table (short↔long), exposed so consumers can reuse the SAME data instead of duplicating
100
100
  * it. The metamorphic gauntlet inverts this table to generate expanded→abbreviated perturbations (`Avenue`→`Ave`); the
101
- * "no load-bearing trivia" rule means that data lives in exactly one place — here.
101
+ * "no required trivia" rule means that data lives in exactly one place — here.
102
102
  */
103
103
  export function abbreviationDictionary(locale) {
104
104
  return getDictionary(locale);
@@ -1 +1 @@
1
- {"version":3,"file":"abbreviations.js","sourceRoot":"","sources":["../abbreviations.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"abbreviations.js","sourceRoot":"","sources":["../lib/abbreviations.ts"],"names":[],"mappings":"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"}
package/out/cjk.d.ts CHANGED
@@ -16,6 +16,9 @@
16
16
  * 1, a full-width `-` always a hyphen — keyboards and copy-paste produce these constantly.
17
17
  * Folding them to ASCII makes `104−0061` and `104-0061` the same input.
18
18
  * - **Fold the ideographic space (U+3000 → ' ').**
19
+ * - **Fold half-width katakana (U+FF61–U+FF9F).** This matches the JP corpus builder. A voiced
20
+ * pair such as `デ` contracts from two UTF-16 units to one `デ`; the output offset maps to the
21
+ * first raw unit in the pair.
19
22
  *
20
23
  * It deliberately does NOT convert **kanji numerals** (一二三…): place names carry numeral kanji as
21
24
  * ordinary characters (三田 _Mita_, 四谷 _Yotsuya_), so a blind 三→3 would corrupt them.
@@ -23,7 +26,7 @@
23
26
  * normalization — deferred. Kana→kanji transliteration (ちょうめ→丁目) is dictionary work and likewise
24
27
  * deferred.
25
28
  *
26
- * Self-gating: a string with none of these characters returns identity, so Latin input is
29
+ * Self-limiting: a string with none of these characters returns identity, so Latin input is
27
30
  * untouched.
28
31
  */
29
32
  export interface CjkResult {
@@ -38,5 +41,7 @@ export interface CjkResult {
38
41
  */
39
42
  stripped: number;
40
43
  }
41
- export declare function applyCjkNormalization(input: string): CjkResult;
44
+ export declare function applyCjkNormalization(input: string, opts?: {
45
+ postalMark?: "strip" | "keep";
46
+ }): CjkResult;
42
47
  //# sourceMappingURL=cjk.d.ts.map
package/out/cjk.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cjk.d.ts","sourceRoot":"","sources":["../cjk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,EAAE,CAAA;IACb;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;CAChB;AAoBD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CA0C9D"}
1
+ {"version":3,"file":"cjk.d.ts","sourceRoot":"","sources":["../lib/cjk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,EAAE,CAAA;IACb;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;CAChB;AA2BD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;CAAO,GAAG,SAAS,CAmE5G"}
package/out/cjk.js CHANGED
@@ -16,6 +16,9 @@
16
16
  * 1, a full-width `-` always a hyphen — keyboards and copy-paste produce these constantly.
17
17
  * Folding them to ASCII makes `104−0061` and `104-0061` the same input.
18
18
  * - **Fold the ideographic space (U+3000 → ' ').**
19
+ * - **Fold half-width katakana (U+FF61–U+FF9F).** This matches the JP corpus builder. A voiced
20
+ * pair such as `デ` contracts from two UTF-16 units to one `デ`; the output offset maps to the
21
+ * first raw unit in the pair.
19
22
  *
20
23
  * It deliberately does NOT convert **kanji numerals** (一二三…): place names carry numeral kanji as
21
24
  * ordinary characters (三田 _Mita_, 四谷 _Yotsuya_), so a blind 三→3 would corrupt them.
@@ -23,10 +26,10 @@
23
26
  * normalization — deferred. Kana→kanji transliteration (ちょうめ→丁目) is dictionary work and likewise
24
27
  * deferred.
25
28
  *
26
- * Self-gating: a string with none of these characters returns identity, so Latin input is
29
+ * Self-limiting: a string with none of these characters returns identity, so Latin input is
27
30
  * untouched.
28
31
  */
29
- import { identityMap } from "./offset-map.js";
32
+ import { identityMap } from "#offset-map";
30
33
  /**
31
34
  * !.
32
35
  */
@@ -44,16 +47,23 @@ const IDEOGRAPHIC_SPACE = 0x30_00;
44
47
  * 〒.
45
48
  */
46
49
  const POSTAL_MARK = 0x30_12;
47
- export function applyCjkNormalization(input) {
50
+ const HALFWIDTH_KATAKANA_START = 0xff_61;
51
+ const HALFWIDTH_KATAKANA_END = 0xff_9f;
52
+ const HALFWIDTH_VOICING_START = 0xff_9e;
53
+ function isHalfwidthKatakana(code) {
54
+ return code >= HALFWIDTH_KATAKANA_START && code <= HALFWIDTH_KATAKANA_END;
55
+ }
56
+ export function applyCjkNormalization(input, opts = {}) {
48
57
  let folded = 0;
49
58
  let stripped = 0;
50
59
  const out = [];
51
60
  const map = [];
61
+ const stripPostalMark = opts.postalMark !== "keep";
52
62
  // All transformed code points are in the BMP (single UTF-16 unit), and every other character is
53
63
  // passed through verbatim, so a per-unit walk is safe for surrogate-pair input too.
54
64
  for (let i = 0; i < input.length; i++) {
55
65
  const code = input.charCodeAt(i);
56
- if (code === POSTAL_MARK) {
66
+ if (code === POSTAL_MARK && stripPostalMark) {
57
67
  stripped += 1;
58
68
  continue; // drop — no addressing content; whitespace collapse later tidies any gap
59
69
  }
@@ -69,6 +79,22 @@ export function applyCjkNormalization(input) {
69
79
  folded += 1;
70
80
  continue;
71
81
  }
82
+ if (isHalfwidthKatakana(code)) {
83
+ const next = input.charCodeAt(i + 1);
84
+ const consumesVoicingMark = code < HALFWIDTH_VOICING_START && next >= HALFWIDTH_VOICING_START && next <= HALFWIDTH_KATAKANA_END;
85
+ const width = consumesVoicingMark ? 2 : 1;
86
+ const normalized = input
87
+ .slice(i, i + width)
88
+ .normalize("NFKC")
89
+ .normalize("NFC");
90
+ for (const unit of normalized) {
91
+ out.push(unit);
92
+ map.push(i);
93
+ }
94
+ folded += width;
95
+ i += width - 1;
96
+ continue;
97
+ }
72
98
  out.push(input[i]);
73
99
  map.push(i);
74
100
  }
package/out/cjk.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cjk.js","sourceRoot":"","sources":["../cjk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAe7C;;GAEG;AACH,MAAM,eAAe,GAAG,OAAO,CAAA;AAC/B;;GAEG;AACH,MAAM,aAAa,GAAG,OAAO,CAAA;AAC7B;;GAEG;AACH,MAAM,kBAAkB,GAAG,OAAO,CAAA;AAClC,MAAM,iBAAiB,GAAG,OAAO,CAAA;AACjC;;GAEG;AACH,MAAM,WAAW,GAAG,OAAO,CAAA;AAE3B,MAAM,UAAU,qBAAqB,CAAC,KAAa;IAClD,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,IAAI,QAAQ,GAAG,CAAC,CAAA;IAChB,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,MAAM,GAAG,GAAa,EAAE,CAAA;IAExB,gGAAgG;IAChG,oFAAoF;IACpF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;QAEhC,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;YAC1B,QAAQ,IAAI,CAAC,CAAA;YAEb,SAAQ,CAAC,yEAAyE;QACnF,CAAC;QAED,IAAI,IAAI,IAAI,eAAe,IAAI,IAAI,IAAI,aAAa,EAAE,CAAC;YACtD,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,GAAG,kBAAkB,CAAC,CAAC,CAAA;YACxD,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACX,MAAM,IAAI,CAAC,CAAA;YAEX,SAAQ;QACT,CAAC;QAED,IAAI,IAAI,KAAK,iBAAiB,EAAE,CAAC;YAChC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YACb,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACX,MAAM,IAAI,CAAC,CAAA;YAEX,SAAQ;QACT,CAAC;QAED,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAA;QACnB,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACZ,CAAC;IAED,IAAI,MAAM,KAAK,CAAC,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;QACpC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAA;IAC/E,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;AACrD,CAAC"}
1
+ {"version":3,"file":"cjk.js","sourceRoot":"","sources":["../lib/cjk.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Comma spacing — a comma directly followed by a letter gains one space, so `Biggin Hill,United Kingdom`
7
+ * reaches stage 2 as `Biggin Hill, United Kingdom`. A tight comma is an ordinary typing pattern, and the parse
8
+ * is the first stage that reads it differently: with no space, the tokenizer glues `,United` into one piece,
9
+ * the segmenter still splits, and the decoder labels the glued piece as a street or a locality it is not.
10
+ *
11
+ * A letter or a digit after the comma triggers the insertion, with one exception: a comma with a digit on
12
+ * BOTH sides is a numeric separator (`12,5`, `1,000`) and is left as typed. `Köln,50733` has a letter before
13
+ * the comma, so it is a list separator and gains the space. A space, punctuation or end of input after the
14
+ * comma is left as typed. The inserted space maps to the comma's own offset, the same rule the `…`
15
+ * expansion in `punctuation.ts` follows, so every span that starts after it still points into the raw input.
16
+ */
17
+ export interface CommaSpacingResult {
18
+ text: string;
19
+ map: number[];
20
+ /**
21
+ * How many spaces were inserted.
22
+ */
23
+ inserted: number;
24
+ }
25
+ /**
26
+ * Insert one space after every comma that is directly followed by a letter or digit, unless the comma is a numeric
27
+ * separator. Offset-map-correct: the inserted space maps to the comma.
28
+ */
29
+ export declare function spaceAfterComma(input: string): CommaSpacingResult;
30
+ //# sourceMappingURL=comma-spacing.d.ts.map