@mailwoman/corpus 4.9.0 → 4.10.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.
@@ -3,77 +3,12 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * Render a `ComponentTag`-keyed dict into a country-localized string.
6
+ * Thin re-export of `@mailwoman/formatter`.
7
7
  *
8
- * Phase 1's adapters carry ground-truth components but need a plausible `raw` string for the model
9
- * to learn from. This module bridges Mailwoman's `ComponentTag` schema to OpenCage's
10
- * `address-formatting` templates (vendored via `@fragaria/address-formatter`, MIT) so adapters
11
- * can synthesize idiomatic per-country output without each one reinventing the template logic.
12
- *
13
- * `@fragaria/address-formatter` returns multi-line strings (newline-separated). This module honors
14
- * that by default and exposes a `separator` option to coerce to single-line when an adapter wants
15
- * `", "` joined output for the corpus.
16
- *
17
- * Limitations (documented, not blockers for Phase 1):
18
- *
19
- * - `unit`: OpenCage's vocabulary doesn't have a slot, so units are appended to the road line
20
- * (`"Pennsylvania Ave NW Apt 4B"`). Phase 2 or beyond can change this if needed.
21
- * - `intersection_a` / `intersection_b`: joined as `"<a> & <b>"` into the road field.
22
- * - `cedex` (FR): folded into `postcode` (e.g. `"75008 CEDEX 08"`) so the FR template renders it in
23
- * the right slot.
24
- * - JP-specific tags (`prefecture`, `municipality`, ...): no mapping yet — left for Phase 6 when JP
25
- * becomes a live locale.
26
- */
27
- import type { ComponentTag } from "@mailwoman/core/types";
28
- /** Options accepted by `formatAddress`. */
29
- export interface FormatAddressOptions {
30
- /**
31
- * Append the country name as a final line (`"USA"`, `"France"`). Default `false`: most corpus
32
- * rows are intra-country and the country prefix is redundant noise.
33
- */
34
- appendCountry?: boolean;
35
- /**
36
- * Apply OpenCage's per-country abbreviation rules (e.g. `"Avenue"` → `"Ave"`, `"Boulevard"` →
37
- * `"Blvd"`). Default `false`. Phase 1 prefers unabbreviated output because synthesis
38
- * (`synthesize.ts`) handles abbreviation swaps as its own augmentation pass.
39
- */
40
- abbreviate?: boolean;
41
- /**
42
- * Replace the template's newlines with this separator. Default `undefined` (keep newlines). Use
43
- * `", "` to get a single-line output, or `" "` to remove all internal punctuation.
44
- */
45
- separator?: string;
46
- }
47
- type ComponentDict = Partial<Record<ComponentTag, string>>;
48
- /**
49
- * After rendering, drop any component whose value isn't actually present in the formatted `raw`.
50
- * OpenCage's per-country templates legitimately omit some inputs:
51
- *
52
- * - **FR**: regions are absorbed by the postcode and rarely render verbatim (`"75008 Paris, France"`
53
- * — `"Île-de-France"` is dropped).
54
- * - **US**: state names are abbreviated (`"Oregon"` → `"OR"`); the verbatim form may not survive.
55
- *
56
- * Alignment downstream requires `components[tag]` to occur in `raw` (within fuzzy-match tolerance).
57
- * If a component was dropped or transformed beyond Levenshtein reach, keeping it in the dict
58
- * guarantees a quarantine reject. Reconciliation prunes the dropouts up front so the row arrives at
59
- * alignment with a self-consistent (`raw`, `components`) pair.
60
- *
61
- * Comparison is case- and whitespace-insensitive. The retained value in `components` is the
62
- * **original** input — alignment will fuzzy-match it against the raw character span.
63
- */
64
- export declare function reconcileComponents(components: ComponentDict, raw: string): ComponentDict;
65
- /**
66
- * Render a component dict into an idiomatic per-country address string.
67
- *
68
- * Returns an empty string if `components` is empty after translation. Throws nothing — bad inputs
69
- * (empty dict, unsupported tag) silently degrade to the longest meaningful prefix.
70
- */
71
- export declare function formatAddress(components: ComponentDict, country: string, opts?: FormatAddressOptions): string;
72
- /**
73
- * Translate a `ComponentTag` dict to the OpenCage vocabulary that `@fragaria/address-formatter`
74
- * expects. Exported for testing and for adapters that want to pre-build the dict for batch
75
- * formatting.
8
+ * The formatter implementation moved to its own workspace (`@mailwoman/formatter`) so the parser,
9
+ * the corpus pipeline, and the record matcher can share one locale-aware renderer plus the
10
+ * canonical match key. This module stays put as the stable `@mailwoman/corpus/format` entry point
11
+ * the synthesis adapters import `formatAddress` / `reconcileComponents` from.
76
12
  */
77
- export declare function toOpenCageComponents(components: ComponentDict, country: string): Record<string, string>;
78
- export {};
13
+ export * from "@mailwoman/formatter";
79
14
  //# sourceMappingURL=format.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAEzD,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACpC;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IAEpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,KAAK,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAA;AAE1D;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,CAWzF;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,oBAAyB,GAAG,MAAM,CAWjH;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA8BvG"}
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,cAAc,sBAAsB,CAAA"}
package/out/src/format.js CHANGED
@@ -3,149 +3,12 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * Render a `ComponentTag`-keyed dict into a country-localized string.
6
+ * Thin re-export of `@mailwoman/formatter`.
7
7
  *
8
- * Phase 1's adapters carry ground-truth components but need a plausible `raw` string for the model
9
- * to learn from. This module bridges Mailwoman's `ComponentTag` schema to OpenCage's
10
- * `address-formatting` templates (vendored via `@fragaria/address-formatter`, MIT) so adapters
11
- * can synthesize idiomatic per-country output without each one reinventing the template logic.
12
- *
13
- * `@fragaria/address-formatter` returns multi-line strings (newline-separated). This module honors
14
- * that by default and exposes a `separator` option to coerce to single-line when an adapter wants
15
- * `", "` joined output for the corpus.
16
- *
17
- * Limitations (documented, not blockers for Phase 1):
18
- *
19
- * - `unit`: OpenCage's vocabulary doesn't have a slot, so units are appended to the road line
20
- * (`"Pennsylvania Ave NW Apt 4B"`). Phase 2 or beyond can change this if needed.
21
- * - `intersection_a` / `intersection_b`: joined as `"<a> & <b>"` into the road field.
22
- * - `cedex` (FR): folded into `postcode` (e.g. `"75008 CEDEX 08"`) so the FR template renders it in
23
- * the right slot.
24
- * - JP-specific tags (`prefecture`, `municipality`, ...): no mapping yet — left for Phase 6 when JP
25
- * becomes a live locale.
26
- */
27
- import addressFormatter from "@fragaria/address-formatter";
28
- /**
29
- * After rendering, drop any component whose value isn't actually present in the formatted `raw`.
30
- * OpenCage's per-country templates legitimately omit some inputs:
31
- *
32
- * - **FR**: regions are absorbed by the postcode and rarely render verbatim (`"75008 Paris, France"`
33
- * — `"Île-de-France"` is dropped).
34
- * - **US**: state names are abbreviated (`"Oregon"` → `"OR"`); the verbatim form may not survive.
35
- *
36
- * Alignment downstream requires `components[tag]` to occur in `raw` (within fuzzy-match tolerance).
37
- * If a component was dropped or transformed beyond Levenshtein reach, keeping it in the dict
38
- * guarantees a quarantine reject. Reconciliation prunes the dropouts up front so the row arrives at
39
- * alignment with a self-consistent (`raw`, `components`) pair.
40
- *
41
- * Comparison is case- and whitespace-insensitive. The retained value in `components` is the
42
- * **original** input — alignment will fuzzy-match it against the raw character span.
43
- */
44
- export function reconcileComponents(components, raw) {
45
- const haystack = raw.toLowerCase().replace(/\s+/g, " ");
46
- const out = {};
47
- for (const [k, v] of Object.entries(components)) {
48
- if (!v)
49
- continue;
50
- const needle = v.toLowerCase().replace(/\s+/g, " ");
51
- if (haystack.includes(needle)) {
52
- out[k] = v;
53
- }
54
- }
55
- return out;
56
- }
57
- /**
58
- * Render a component dict into an idiomatic per-country address string.
59
- *
60
- * Returns an empty string if `components` is empty after translation. Throws nothing — bad inputs
61
- * (empty dict, unsupported tag) silently degrade to the longest meaningful prefix.
62
- */
63
- export function formatAddress(components, country, opts = {}) {
64
- const ocComponents = toOpenCageComponents(components, country);
65
- if (Object.keys(ocComponents).length === 0)
66
- return "";
67
- const raw = addressFormatter.format(ocComponents, {
68
- abbreviate: opts.abbreviate ?? false,
69
- appendCountry: opts.appendCountry ?? false,
70
- });
71
- const trimmed = raw.replace(/\s+$/g, "");
72
- return opts.separator !== undefined ? trimmed.replace(/\n+/g, opts.separator) : trimmed;
73
- }
74
- /**
75
- * Translate a `ComponentTag` dict to the OpenCage vocabulary that `@fragaria/address-formatter`
76
- * expects. Exported for testing and for adapters that want to pre-build the dict for batch
77
- * formatting.
78
- */
79
- export function toOpenCageComponents(components, country) {
80
- const out = {};
81
- const road = composeRoad(components);
82
- if (road)
83
- out.road = road;
84
- if (components.house_number)
85
- out.house_number = components.house_number;
86
- if (components.venue)
87
- out.house = components.venue;
88
- if (components.locality)
89
- out.city = components.locality;
90
- if (components.dependent_locality)
91
- out.suburb = components.dependent_locality;
92
- if (components.subregion)
93
- out.county = components.subregion;
94
- if (components.region)
95
- out.state = components.region;
96
- const postcode = composePostcode(components);
97
- if (postcode)
98
- out.postcode = postcode;
99
- if (components.po_box)
100
- out.po_box = components.po_box;
101
- if (components.attention)
102
- out.attention = components.attention;
103
- if (components.country)
104
- out.country = components.country;
105
- // country_code drives template selection, not output. Only emit it if at least one other
106
- // component is present — otherwise the FR/US templates render the bare code as a fallback
107
- // line ("US"), which is never what a corpus consumer wants.
108
- const cc = country.trim().toLowerCase();
109
- if (cc && Object.keys(out).length > 0)
110
- out.country_code = cc;
111
- return out;
112
- }
113
- /**
114
- * Build the `road` line from prefix / particle / street / suffix / unit / intersection components.
115
- * Order:
116
- *
117
- * ```
118
- * [intersection_a & intersection_b]
119
- * OR
120
- * [street_prefix] [street_prefix_particle] [street] [street_suffix] [unit]
121
- * ```
122
- */
123
- function composeRoad(components) {
124
- if (components.intersection_a && components.intersection_b) {
125
- return `${components.intersection_a} & ${components.intersection_b}`;
126
- }
127
- const parts = [];
128
- if (components.street_prefix)
129
- parts.push(components.street_prefix);
130
- if (components.street_prefix_particle)
131
- parts.push(components.street_prefix_particle);
132
- if (components.street)
133
- parts.push(components.street);
134
- if (components.street_suffix)
135
- parts.push(components.street_suffix);
136
- if (components.unit)
137
- parts.push(components.unit);
138
- return parts.join(" ").replace(/\s+/g, " ").trim();
139
- }
140
- /**
141
- * Fold CEDEX into postcode for FR-style output: `"75008"` + cedex `"CEDEX 08"` → `"75008 CEDEX
142
- * 08"`. If only one is present, return it. If neither, return empty.
8
+ * The formatter implementation moved to its own workspace (`@mailwoman/formatter`) so the parser,
9
+ * the corpus pipeline, and the record matcher can share one locale-aware renderer plus the
10
+ * canonical match key. This module stays put as the stable `@mailwoman/corpus/format` entry point
11
+ * the synthesis adapters import `formatAddress` / `reconcileComponents` from.
143
12
  */
144
- function composePostcode(components) {
145
- const base = components.postcode?.trim() ?? "";
146
- const cedex = components.cedex?.trim() ?? "";
147
- if (base && cedex)
148
- return `${base} ${cedex}`.replace(/\s+/g, " ");
149
- return base || cedex;
150
- }
13
+ export * from "@mailwoman/formatter";
151
14
  //# sourceMappingURL=format.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/format.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,cAAc,sBAAsB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mailwoman/corpus",
3
- "version": "4.9.0",
3
+ "version": "4.10.0",
4
4
  "description": "Mailwoman corpus pipeline: BIO-labeled dataset builder for the neural classifier.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -17,9 +17,9 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@dsnp/parquetjs": "1.8.7",
20
- "@fragaria/address-formatter": "^6.7.1",
21
- "@mailwoman/codex": "4.9.0",
22
- "@mailwoman/core": "4.9.0",
20
+ "@mailwoman/codex": "4.10.0",
21
+ "@mailwoman/core": "4.10.0",
22
+ "@mailwoman/formatter": "4.10.0",
23
23
  "csv-parse": "^5.6.0",
24
24
  "fast-glob": "^3.3.3",
25
25
  "fastest-levenshtein": "^1.0.16",