@mailwoman/corpus 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.
- package/package.json +59 -27
- package/src/adapter.ts +153 -0
- package/src/adapters/ban/adapter.ts +189 -0
- package/src/adapters/ban/street-decompose.ts +98 -0
- package/src/adapters/fcc-bdc/adapter.ts +188 -0
- package/src/adapters/geonames/adapter.ts +185 -0
- package/src/adapters/geonames-postal/adapter.ts +111 -0
- package/src/adapters/gnaf/adapter.ts +139 -0
- package/src/adapters/gnaf/assemble.ts +200 -0
- package/src/adapters/index.ts +135 -0
- package/src/adapters/openaddresses/adapter.ts +237 -0
- package/src/adapters/overture/adapter.ts +149 -0
- package/src/adapters/state-hi-schools/adapter.ts +171 -0
- package/src/adapters/state-ia-contractors/adapter.ts +148 -0
- package/src/adapters/state-ny-notaries/adapter.ts +169 -0
- package/src/adapters/state-tx-notaries/adapter.ts +160 -0
- package/src/adapters/synth-po-box/adapter.ts +197 -0
- package/src/adapters/tiger/adapter.ts +232 -0
- package/src/adapters/tiger/street-decompose.ts +120 -0
- package/src/adapters/usgov-hrsa-fqhc/adapter.ts +189 -0
- package/src/adapters/usgov-imls-pls/adapter.ts +152 -0
- package/src/adapters/usgov-irs-bmf/adapter.ts +155 -0
- package/src/adapters/usgov-nad/adapter.ts +338 -0
- package/src/adapters/usgov-nppes/adapter.ts +169 -0
- package/src/adapters/usgov-samhsa-treatment-locator/adapter.ts +211 -0
- package/src/adapters/wof-admin-jp/adapter.ts +176 -0
- package/src/adapters/wof-admin-json/adapter.ts +280 -0
- package/src/adapters/wof-postalcode-json/adapter.ts +209 -0
- package/src/align.ts +317 -0
- package/src/build.ts +348 -0
- package/src/codex/us-fips-state.ts +109 -0
- package/src/format.ts +14 -0
- package/src/golden.ts +147 -0
- package/src/index.ts +28 -0
- package/src/license.ts +48 -0
- package/src/parquet-wrapper/index.ts +9 -0
- package/src/parquet-wrapper/reader.ts +67 -0
- package/src/parquet-wrapper/schema.ts +80 -0
- package/src/parquet-wrapper/writer.ts +102 -0
- package/src/parquet.ts +360 -0
- package/src/runner.ts +283 -0
- package/src/shard-recipes/anchor-absorption.ts +86 -0
- package/src/shard-recipes/boundary-stress.ts +87 -0
- package/src/shard-recipes/country-balanced.ts +471 -0
- package/src/shard-recipes/cz-pcfirst-preposition.ts +83 -0
- package/src/shard-recipes/fr-admin-split.ts +212 -0
- package/src/shard-recipes/fr-bare-street.ts +85 -0
- package/src/shard-recipes/fr-fragment.ts +304 -0
- package/src/shard-recipes/fr-order.ts +283 -0
- package/src/shard-recipes/german.ts +206 -0
- package/src/shard-recipes/house-venue.ts +78 -0
- package/src/shard-recipes/index.ts +76 -0
- package/src/shard-recipes/intersection.ts +617 -0
- package/src/shard-recipes/locale.ts +415 -0
- package/src/shard-recipes/nl-postcode.ts +105 -0
- package/src/shard-recipes/no-fragment.ts +206 -0
- package/src/shard-recipes/no-street-led.ts +130 -0
- package/src/shard-recipes/no-street.ts +78 -0
- package/src/shard-recipes/po-box-cedex.ts +853 -0
- package/src/shard-recipes/po-box.ts +111 -0
- package/src/shard-recipes/scaffold.ts +187 -0
- package/src/shard-recipes/si-bare-village.ts +88 -0
- package/src/shard-recipes/street-affix.ts +549 -0
- package/src/shard-recipes/street-bare.ts +90 -0
- package/src/shard-recipes/street.ts +74 -0
- package/src/shard-recipes/unit.ts +315 -0
- package/src/split.ts +251 -0
- package/src/synthesize-anchor-absorption.ts +263 -0
- package/src/synthesize-boundary-stress.ts +504 -0
- package/src/synthesize-german.ts +227 -0
- package/src/synthesize-house-venue.ts +192 -0
- package/src/synthesize-intersection.ts +205 -0
- package/src/synthesize-no-street.ts +341 -0
- package/src/synthesize-po-box.ts +307 -0
- package/src/synthesize-street.ts +264 -0
- package/src/synthesize.ts +872 -0
- package/src/tokenize.ts +68 -0
- package/src/tools/align-shard.ts +67 -0
- package/src/tools/audit.ts +352 -0
- package/src/tools/corpus-stats.ts +183 -0
- package/src/tools/fetch/ban.ts +288 -0
- package/src/tools/fetch/download.ts +119 -0
- package/src/tools/fetch/hrsa.ts +65 -0
- package/src/tools/fetch/imls-pls.ts +162 -0
- package/src/tools/fetch/index.ts +121 -0
- package/src/tools/fetch/nad.ts +304 -0
- package/src/tools/fetch/nppes.ts +177 -0
- package/src/tools/fetch/openaddresses.ts +380 -0
- package/src/tools/fetch/state-hi-schools.ts +217 -0
- package/src/tools/fetch/state-sources.ts +142 -0
- package/src/tools/fetch/tiger-full.ts +318 -0
- package/src/tools/golden-expand.ts +524 -0
- package/src/tools/golden-promote.ts +304 -0
- package/src/tools/index.ts +24 -0
- package/src/tools/ingest-csv.ts +420 -0
- package/src/tools/jsonl-to-parquet.ts +200 -0
- package/src/tools/lint-shard-vocab.ts +357 -0
- package/src/tools/lint-shard.ts +502 -0
- package/src/tools/overlay-manifest.ts +141 -0
- package/src/tools/shard-kryptonite.ts +131 -0
- package/src/tools/shard-translit.ts +244 -0
- package/src/types.ts +213 -0
- package/src/wof-json.ts +236 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `country-balanced` shard recipe — the BALANCED, MODEL-FIRST country-coverage shard (#464). The
|
|
7
|
+
* shipped model is STARVED on `country` (P=R=F1=0 on the homograph eval), so this fills the void
|
|
8
|
+
* the way the `unit` shard did, but built to AVOID over-firing "trailing token ⇒ country". Three
|
|
9
|
+
* ingredients, ported faithfully from scripts/build-country-shard-balanced.mjs:
|
|
10
|
+
*
|
|
11
|
+
* 1. Breadth/recall — real OA skeletons (US/DE/FR/IT/NL) with a country token in a varied surface form
|
|
12
|
+
* from `@mailwoman/codex/country` (canonical / endonym / ISO code), + ~30% country-ABSENT
|
|
13
|
+
* negatives (teach O-emission, the precision floor).
|
|
14
|
+
* 2. Homograph CONTRAST pairs — each true country-name homograph (Georgia, Jordan, Lebanon, Mexico,
|
|
15
|
+
* Peru, Turkey) rendered BOTH ways: as `country` (foreign-city context) AND as the US
|
|
16
|
+
* `region`/`locality` (US-ZIP context). Teaches that the label is CONTEXTUAL, not
|
|
17
|
+
* positional.
|
|
18
|
+
* 3. Code-as-region negatives — 2-letter codes that are both a US state abbrev and an ISO country code
|
|
19
|
+
* (CA/GA/IN/MA/PA/AL) in US-ZIP context → must read as `region`, never `country`.
|
|
20
|
+
*
|
|
21
|
+
* `--golden` emits a held-out synthetic val over the VT (US) + Berlin (DE) holdouts. This is a
|
|
22
|
+
* `generate`-mode recipe that still reads REAL tuples off disk — `--count` bounds the OUTPUT, not
|
|
23
|
+
* the input. The passed `random` (the framework LCG) is consumed in the exact call order the
|
|
24
|
+
* legacy script used.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { spawnSync } from "node:child_process"
|
|
28
|
+
|
|
29
|
+
import { COUNTRY_SURFACE_FORMS, CountryNames } from "@mailwoman/codex/country"
|
|
30
|
+
import type { ComponentTag } from "@mailwoman/core/types"
|
|
31
|
+
|
|
32
|
+
import { stableSourceID } from "../adapter.ts"
|
|
33
|
+
import { alignRow } from "../align.ts"
|
|
34
|
+
import type { CanonicalRow } from "../types.ts"
|
|
35
|
+
import { makeMulberry32, type ShardRecipe } from "./scaffold.ts"
|
|
36
|
+
|
|
37
|
+
// v2: the country TOKEN is decoupled from the skeleton's locale and drawn from a BROAD pool — every
|
|
38
|
+
// ISO canonical name + every curated surface form (endonyms/abbrevs). Surface forms are over-weighted
|
|
39
|
+
// so endonyms/abbrevs ("Deutschland","USA","NL") get strong signal.
|
|
40
|
+
const COUNTRY_FORM_POOL = (() => {
|
|
41
|
+
const surface = Object.values(COUNTRY_SURFACE_FORMS).flat() // endonyms + abbrevs + canonical (curated)
|
|
42
|
+
const names = [...CountryNames]
|
|
43
|
+
|
|
44
|
+
// all ~249 ISO canonical English names (breadth)
|
|
45
|
+
return { surface, names }
|
|
46
|
+
})()
|
|
47
|
+
const COUNTRY_ABSENT_PROB = 0.3 // negatives: rows with NO country token → teach golden precision
|
|
48
|
+
|
|
49
|
+
/** A cached OpenAddresses extract + the implied iso2/region/render-order. */
|
|
50
|
+
interface CountrySource {
|
|
51
|
+
zip: string
|
|
52
|
+
csv: string
|
|
53
|
+
iso2: string
|
|
54
|
+
region: string
|
|
55
|
+
order: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Multi-locale OA sources. region = implied admin where the extract is single-region (US states, DE
|
|
59
|
+
// Saxony); countrywide extracts (FR/IT/NL) read region from the CSV when present.
|
|
60
|
+
const SOURCES: readonly CountrySource[] = [
|
|
61
|
+
{ zip: "/tmp/oa-cache/us__ia__statewide.zip", csv: "us/ia/statewide.csv", iso2: "US", region: "IA", order: "us" },
|
|
62
|
+
{ zip: "/tmp/oa-cache/us__il__cook.zip", csv: "us/il/cook.csv", iso2: "US", region: "IL", order: "us" },
|
|
63
|
+
{ zip: "/tmp/oa-cache/us__mt__statewide.zip", csv: "us/mt/statewide.csv", iso2: "US", region: "MT", order: "us" },
|
|
64
|
+
{ zip: "/tmp/oa-cache/us__sd__statewide.zip", csv: "us/sd/statewide.csv", iso2: "US", region: "SD", order: "us" },
|
|
65
|
+
{ zip: "/tmp/oa-cache/de__sn__statewide.zip", csv: "de/sn/statewide.csv", iso2: "DE", region: "", order: "eu" },
|
|
66
|
+
{ zip: "/tmp/oa-cache/fr__countrywide.zip", csv: "fr/countrywide.csv", iso2: "FR", region: "", order: "fr" },
|
|
67
|
+
// ES uses the Spanish IGN schema, not the OA standard columns — skipped here (codex still recognizes
|
|
68
|
+
// "España"/"Spain"). A dedicated IGN adapter is a follow-up.
|
|
69
|
+
{ zip: "/tmp/oa-cache/it__countrywide.zip", csv: "it/countrywide.csv", iso2: "IT", region: "", order: "eu" },
|
|
70
|
+
{ zip: "/tmp/oa-cache/nl__countrywide.zip", csv: "nl/countrywide.csv", iso2: "NL", region: "", order: "eu" },
|
|
71
|
+
]
|
|
72
|
+
// Held-out for --golden: Vermont (US holdout) + Berlin (DE holdout) — geographic split, never trained.
|
|
73
|
+
const EVAL_SOURCES: readonly CountrySource[] = [
|
|
74
|
+
{ zip: "/tmp/oa-cache/us__vt__statewide.zip", csv: "us/vt/statewide.csv", iso2: "US", region: "VT", order: "us" },
|
|
75
|
+
{ zip: "/tmp/oa-cache/de__berlin.zip", csv: "de/berlin.csv", iso2: "DE", region: "", order: "eu" },
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
/** A real tuple read out of a cached OA zip (+ the source's iso2/render-order). */
|
|
79
|
+
interface CountryTuple {
|
|
80
|
+
house_number: string
|
|
81
|
+
street: string
|
|
82
|
+
locality: string
|
|
83
|
+
region: string
|
|
84
|
+
postcode: string
|
|
85
|
+
iso2: string
|
|
86
|
+
order: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function splitCSV(line: string): string[] {
|
|
90
|
+
const out: string[] = []
|
|
91
|
+
let cur = "",
|
|
92
|
+
inQ = false
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < line.length; i++) {
|
|
95
|
+
const c = line[i]
|
|
96
|
+
|
|
97
|
+
if (inQ) {
|
|
98
|
+
if (c === '"') {
|
|
99
|
+
if (line[i + 1] === '"') {
|
|
100
|
+
cur += '"'
|
|
101
|
+
i++
|
|
102
|
+
} else {
|
|
103
|
+
inQ = false
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
cur += c
|
|
107
|
+
}
|
|
108
|
+
} else if (c === '"') {
|
|
109
|
+
inQ = true
|
|
110
|
+
} else if (c === ",") {
|
|
111
|
+
out.push(cur)
|
|
112
|
+
cur = ""
|
|
113
|
+
} else {
|
|
114
|
+
cur += c
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
out.push(cur)
|
|
118
|
+
|
|
119
|
+
return out
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readTuples(source: CountrySource, limit: number): CountryTuple[] {
|
|
123
|
+
// countrywide extracts (FR/IT/NL) are GB-scale — cap the bytes with `head` (read ~8 lines per wanted
|
|
124
|
+
// tuple to survive dedup/skips) so the toString stays under V8's string limit.
|
|
125
|
+
const maxLines = Math.max(limit * 8, 20000) + 1
|
|
126
|
+
const r = spawnSync("bash", ["-c", `unzip -p "${source.zip}" "${source.csv}" | head -n ${maxLines}`], {
|
|
127
|
+
maxBuffer: 1024 * 1024 * 1024,
|
|
128
|
+
encoding: "buffer",
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
if (r.status !== 0) {
|
|
132
|
+
console.error(` WARN: unzip failed for ${source.zip} (status ${r.status})`)
|
|
133
|
+
|
|
134
|
+
return []
|
|
135
|
+
}
|
|
136
|
+
const lines = r.stdout.toString("utf8").split(/\r?\n/)
|
|
137
|
+
|
|
138
|
+
if (lines.length < 2) return []
|
|
139
|
+
const header = splitCSV(lines[0]!).map((h) => h.trim().toLowerCase())
|
|
140
|
+
const idx = (n: string): number => header.indexOf(n)
|
|
141
|
+
const iNum = idx("number"),
|
|
142
|
+
iStreet = idx("street"),
|
|
143
|
+
iCity = idx("city"),
|
|
144
|
+
iRegion = idx("region"),
|
|
145
|
+
iPost = idx("postcode")
|
|
146
|
+
const get = (cells: string[], i: number): string => (i >= 0 && i < cells.length ? (cells[i] ?? "").trim() : "")
|
|
147
|
+
const tuples: CountryTuple[] = []
|
|
148
|
+
const seen = new Set<string>()
|
|
149
|
+
|
|
150
|
+
for (let li = 1; li < lines.length && tuples.length < limit; li++) {
|
|
151
|
+
if (!lines[li]) continue
|
|
152
|
+
const cells = splitCSV(lines[li]!)
|
|
153
|
+
const street = get(cells, iStreet),
|
|
154
|
+
locality = get(cells, iCity),
|
|
155
|
+
house_number = get(cells, iNum)
|
|
156
|
+
|
|
157
|
+
if (!street || !locality || !house_number) continue
|
|
158
|
+
const key = `${house_number}|${street}|${locality}`.toLowerCase()
|
|
159
|
+
|
|
160
|
+
if (seen.has(key)) continue
|
|
161
|
+
seen.add(key)
|
|
162
|
+
tuples.push({
|
|
163
|
+
house_number,
|
|
164
|
+
street,
|
|
165
|
+
locality,
|
|
166
|
+
region: get(cells, iRegion) || source.region,
|
|
167
|
+
postcode: get(cells, iPost),
|
|
168
|
+
iso2: source.iso2,
|
|
169
|
+
order: source.order,
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return tuples
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Pick a country token from the BROAD pool, or null (a country-absent negative). v2. */
|
|
177
|
+
function pickCountry(random: () => number): string | null {
|
|
178
|
+
if (random() < COUNTRY_ABSENT_PROB) return null // negative — teaches "trailing token != always country"
|
|
179
|
+
// 60% curated surface forms (endonym/abbrev variety), 40% broad ISO canonical names (coverage).
|
|
180
|
+
const pool = random() < 0.6 ? COUNTRY_FORM_POOL.surface : COUNTRY_FORM_POOL.names
|
|
181
|
+
|
|
182
|
+
return pool[Math.floor(random() * pool.length)]!
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Render the address body in native-ish order. `country` null → a country-ABSENT negative row. */
|
|
186
|
+
function renderCountry(
|
|
187
|
+
random: () => number,
|
|
188
|
+
t: CountryTuple,
|
|
189
|
+
country: string | null
|
|
190
|
+
): { fmt: string; raw: string; components: Partial<Record<ComponentTag, string>> } {
|
|
191
|
+
const { house_number: hn, street, locality: loc, region: reg, postcode: pc, order } = t
|
|
192
|
+
const components: Partial<Record<ComponentTag, string>> = { house_number: hn, street, locality: loc }
|
|
193
|
+
|
|
194
|
+
if (reg) {
|
|
195
|
+
components.region = reg
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (pc) {
|
|
199
|
+
components.postcode = pc
|
|
200
|
+
}
|
|
201
|
+
let body: string
|
|
202
|
+
|
|
203
|
+
if (order === "us") {
|
|
204
|
+
const regPc = [reg, pc].filter(Boolean).join(" ")
|
|
205
|
+
body = `${hn} ${street}, ${loc}${regPc ? ", " + regPc : ""}`
|
|
206
|
+
} else if (order === "fr") {
|
|
207
|
+
// French is NUMBER-street, postcode-city: "84 Route de la Fontaine, 75008 Paris".
|
|
208
|
+
body = `${hn} ${street}, ${[pc, loc].filter(Boolean).join(" ")}`
|
|
209
|
+
} else {
|
|
210
|
+
// DE/IT/NL: street-number, postcode-city: "Pariser Platz 1, 10117 Berlin".
|
|
211
|
+
const pcCity = [pc, loc].filter(Boolean).join(" ")
|
|
212
|
+
body = `${street} ${hn}, ${pcCity}`
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!country) {
|
|
216
|
+
// Negative: a normal address, NO country token/component. Teaches that a trailing region/city/
|
|
217
|
+
// postcode is NOT a country (counters the v1 golden over-firing).
|
|
218
|
+
return { fmt: "negative", raw: body, components }
|
|
219
|
+
}
|
|
220
|
+
const withC: Partial<Record<ComponentTag, string>> = { ...components, country }
|
|
221
|
+
const r = random()
|
|
222
|
+
|
|
223
|
+
if (r < 0.8) return { fmt: "full", raw: `${body}, ${country}`, components: withC }
|
|
224
|
+
|
|
225
|
+
if (r < 0.92) return { fmt: "full-nl", raw: `${body}\n${country}`, components: withC }
|
|
226
|
+
const bareBody = order === "us" || order === "fr" ? `${hn} ${street}, ${loc}` : `${street} ${hn}, ${loc}`
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
fmt: "bare",
|
|
230
|
+
raw: `${bareBody}, ${country}`,
|
|
231
|
+
components: { house_number: hn, street, locality: loc, country },
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Homograph contrast (the model-first addition) ───────────────────────────────────────────────
|
|
236
|
+
// True country-name homographs: the surface form is BOTH a country AND a US state/locality. Rendering
|
|
237
|
+
// each BOTH ways (foreign-city → country; US-ZIP → region/locality) is what teaches the CONTEXTUAL
|
|
238
|
+
// distinction. role: how the surface reads in US context.
|
|
239
|
+
interface Homograph {
|
|
240
|
+
surface: string
|
|
241
|
+
iso2: string
|
|
242
|
+
cities: readonly string[]
|
|
243
|
+
us: { role: "region" | "locality"; locality: string; region: string; postcodes: readonly string[] }
|
|
244
|
+
}
|
|
245
|
+
const HOMOGRAPHS: readonly Homograph[] = [
|
|
246
|
+
{
|
|
247
|
+
surface: "Georgia",
|
|
248
|
+
iso2: "GE",
|
|
249
|
+
cities: ["Tbilisi", "Batumi", "Kutaisi", "Rustavi"],
|
|
250
|
+
us: { role: "region", locality: "Atlanta", region: "Georgia", postcodes: ["30309", "31401", "30601", "31201"] },
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
surface: "Jordan",
|
|
254
|
+
iso2: "JO",
|
|
255
|
+
cities: ["Amman", "Irbid", "Zarqa", "Aqaba"],
|
|
256
|
+
us: { role: "locality", locality: "Jordan", region: "MN", postcodes: ["55352"] },
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
surface: "Lebanon",
|
|
260
|
+
iso2: "LB",
|
|
261
|
+
cities: ["Beirut", "Tripoli", "Sidon", "Byblos"],
|
|
262
|
+
us: { role: "locality", locality: "Lebanon", region: "TN", postcodes: ["37087", "03766", "17042", "45036"] },
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
surface: "Mexico",
|
|
266
|
+
iso2: "MX",
|
|
267
|
+
cities: ["Guadalajara", "Monterrey", "Puebla", "Oaxaca"],
|
|
268
|
+
us: { role: "locality", locality: "Mexico", region: "MO", postcodes: ["65265"] },
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
surface: "Peru",
|
|
272
|
+
iso2: "PE",
|
|
273
|
+
cities: ["Cusco", "Arequipa", "Trujillo", "Iquitos"],
|
|
274
|
+
us: { role: "locality", locality: "Peru", region: "IL", postcodes: ["61354", "46970"] },
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
surface: "Turkey",
|
|
278
|
+
iso2: "TR",
|
|
279
|
+
cities: ["Ankara", "Izmir", "Bursa", "Antalya"],
|
|
280
|
+
us: { role: "locality", locality: "Turkey", region: "TX", postcodes: ["79261", "28393"] },
|
|
281
|
+
},
|
|
282
|
+
]
|
|
283
|
+
// 2-letter codes that are BOTH a US state abbrev AND an ISO country code → must read as region in US ctx.
|
|
284
|
+
interface AbbrevRegion {
|
|
285
|
+
code: string
|
|
286
|
+
localities: readonly string[]
|
|
287
|
+
postcodes: readonly string[]
|
|
288
|
+
}
|
|
289
|
+
const ABBREV_REGIONS: readonly AbbrevRegion[] = [
|
|
290
|
+
{ code: "CA", localities: ["Los Angeles", "Sacramento", "San Diego"], postcodes: ["90012", "95814", "92101"] }, // California / Canada
|
|
291
|
+
{ code: "GA", localities: ["Atlanta", "Savannah", "Macon"], postcodes: ["30309", "31401", "31201"] }, // Georgia(US) / Georgia
|
|
292
|
+
{ code: "IN", localities: ["Indianapolis", "Fort Wayne"], postcodes: ["46204", "46802"] }, // Indiana / India
|
|
293
|
+
{ code: "MA", localities: ["Boston", "Worcester"], postcodes: ["02108", "01608"] }, // Massachusetts / Morocco
|
|
294
|
+
{ code: "PA", localities: ["Philadelphia", "Pittsburgh"], postcodes: ["19103", "15222"] }, // Pennsylvania / Panama
|
|
295
|
+
{ code: "AL", localities: ["Birmingham", "Montgomery"], postcodes: ["35203", "36104"] }, // Alabama / Albania
|
|
296
|
+
]
|
|
297
|
+
const STREET_POOL: readonly string[] = [
|
|
298
|
+
"Main Street",
|
|
299
|
+
"Oak Avenue",
|
|
300
|
+
"Park Road",
|
|
301
|
+
"Elm Street",
|
|
302
|
+
"Hill Road",
|
|
303
|
+
"Market Street",
|
|
304
|
+
"Church Street",
|
|
305
|
+
"King Street",
|
|
306
|
+
"2nd Avenue",
|
|
307
|
+
"Maple Drive",
|
|
308
|
+
]
|
|
309
|
+
const pick = <T>(random: () => number, arr: readonly T[]): T => arr[Math.floor(random() * arr.length)]!
|
|
310
|
+
const houseNo = (random: () => number): string => String(1 + Math.floor(random() * 998))
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* A homograph CONTRAST row: ~half render the surface as `country` (foreign city), half as the US `region`/`locality`
|
|
314
|
+
* (US ZIP, NO country). Returns iso2 for provenance.
|
|
315
|
+
*/
|
|
316
|
+
function renderHomograph(random: () => number): {
|
|
317
|
+
fmt: string
|
|
318
|
+
raw: string
|
|
319
|
+
components: Partial<Record<ComponentTag, string>>
|
|
320
|
+
iso2: string
|
|
321
|
+
} {
|
|
322
|
+
const h = pick(random, HOMOGRAPHS)
|
|
323
|
+
const hn = houseNo(random),
|
|
324
|
+
street = pick(random, STREET_POOL)
|
|
325
|
+
|
|
326
|
+
if (random() < 0.5) {
|
|
327
|
+
const city = pick(random, h.cities)
|
|
328
|
+
const withStreet = random() < 0.6
|
|
329
|
+
const raw = withStreet ? `${hn} ${street}, ${city}, ${h.surface}` : `${city}, ${h.surface}`
|
|
330
|
+
const components: Partial<Record<ComponentTag, string>> = withStreet
|
|
331
|
+
? { house_number: hn, street, locality: city, country: h.surface }
|
|
332
|
+
: { locality: city, country: h.surface }
|
|
333
|
+
|
|
334
|
+
return { fmt: "homograph-country", raw, components, iso2: h.iso2 }
|
|
335
|
+
}
|
|
336
|
+
const pc = pick(random, h.us.postcodes)
|
|
337
|
+
|
|
338
|
+
if (h.us.role === "region") {
|
|
339
|
+
// surface is the US STATE: "123 Oak Ave, Atlanta, Georgia 30309" → region, no country
|
|
340
|
+
return {
|
|
341
|
+
fmt: "homograph-us-region",
|
|
342
|
+
raw: `${hn} ${street}, ${h.us.locality}, ${h.surface} ${pc}`,
|
|
343
|
+
components: { house_number: hn, street, locality: h.us.locality, region: h.surface, postcode: pc },
|
|
344
|
+
iso2: "US",
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// surface is the US CITY: "123 Oak Ave, Lebanon, TN 37087" → locality, no country
|
|
349
|
+
return {
|
|
350
|
+
fmt: "homograph-us-locality",
|
|
351
|
+
raw: `${hn} ${street}, ${h.surface}, ${h.us.region} ${pc}`,
|
|
352
|
+
components: { house_number: hn, street, locality: h.surface, region: h.us.region, postcode: pc },
|
|
353
|
+
iso2: "US",
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** An abbrev-as-region negative: "123 Main St, Los Angeles, CA 90012" → region CA, NO country. */
|
|
358
|
+
function renderAbbrevRegion(random: () => number): {
|
|
359
|
+
fmt: string
|
|
360
|
+
raw: string
|
|
361
|
+
components: Partial<Record<ComponentTag, string>>
|
|
362
|
+
iso2: string
|
|
363
|
+
} {
|
|
364
|
+
const a = pick(random, ABBREV_REGIONS)
|
|
365
|
+
const hn = houseNo(random),
|
|
366
|
+
street = pick(random, STREET_POOL),
|
|
367
|
+
locality = pick(random, a.localities),
|
|
368
|
+
postcode = pick(random, a.postcodes)
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
fmt: "abbrev-region",
|
|
372
|
+
raw: `${hn} ${street}, ${locality}, ${a.code} ${postcode}`,
|
|
373
|
+
components: { house_number: hn, street, locality, region: a.code, postcode },
|
|
374
|
+
iso2: "US",
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const HOMOGRAPH_FRAC = 0.22 // share of rows that are homograph contrast pairs
|
|
379
|
+
const ABBREV_FRAC = 0.08 // share that are code-as-region negatives (cumulative with HOMOGRAPH_FRAC)
|
|
380
|
+
|
|
381
|
+
export const countryBalancedRecipe: ShardRecipe = {
|
|
382
|
+
name: "country-balanced",
|
|
383
|
+
description: "Balanced model-first country rows (#464): OA skeletons + ISO surface forms + homograph contrast pairs",
|
|
384
|
+
mode: "generate",
|
|
385
|
+
options: [{ flag: "--golden", description: "Emit the held-out VT+Berlin eval slice" }],
|
|
386
|
+
async run(opts, write) {
|
|
387
|
+
if (opts.count == null) throw new Error("country-balanced recipe requires --count <N>")
|
|
388
|
+
const count = opts.count
|
|
389
|
+
// Legacy build-country-shard-balanced.mjs seeded mulberry32 with the raw seed: `const random = mulberry32(opts.seed)`.
|
|
390
|
+
const random = makeMulberry32(opts.seed)
|
|
391
|
+
const source = opts.sourceName ?? "synth-country"
|
|
392
|
+
const sources = opts.golden ? EVAL_SOURCES : SOURCES
|
|
393
|
+
const perSource = Math.ceil((count * 3) / sources.length) // over-read; balance locales
|
|
394
|
+
|
|
395
|
+
const pool: CountryTuple[] = []
|
|
396
|
+
|
|
397
|
+
for (const s of sources) {
|
|
398
|
+
const t = readTuples(s, perSource)
|
|
399
|
+
console.error(` ${s.csv} (${s.iso2}): ${t.length} tuples`)
|
|
400
|
+
|
|
401
|
+
for (const x of t) {
|
|
402
|
+
pool.push(x)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (pool.length === 0) {
|
|
407
|
+
throw new Error("No tuples — are the cached OA zips present in /tmp/oa-cache?")
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let emitted = 0
|
|
411
|
+
let skipped = 0
|
|
412
|
+
let guard = 0
|
|
413
|
+
const N = pool.length
|
|
414
|
+
|
|
415
|
+
while (emitted < count && guard++ < count * 8) {
|
|
416
|
+
// Mix three row types: homograph contrast (the distinction), code-as-region negatives, and the
|
|
417
|
+
// breadth/recall main path (random ISO form on an OA skeleton, ~30% country-absent).
|
|
418
|
+
const roll = random()
|
|
419
|
+
let rendered: { fmt: string; raw: string; components: Partial<Record<ComponentTag, string>> }
|
|
420
|
+
let rowISO2: string
|
|
421
|
+
|
|
422
|
+
if (roll < HOMOGRAPH_FRAC) {
|
|
423
|
+
const h = renderHomograph(random)
|
|
424
|
+
rendered = h
|
|
425
|
+
rowISO2 = h.iso2
|
|
426
|
+
} else if (roll < HOMOGRAPH_FRAC + ABBREV_FRAC) {
|
|
427
|
+
const a = renderAbbrevRegion(random)
|
|
428
|
+
rendered = a
|
|
429
|
+
rowISO2 = a.iso2
|
|
430
|
+
} else {
|
|
431
|
+
const t = pool[Math.floor(random() * N)]!
|
|
432
|
+
const country = pickCountry(random) // may be null → a country-absent negative row
|
|
433
|
+
rendered = renderCountry(random, t, country)
|
|
434
|
+
rowISO2 = t.iso2
|
|
435
|
+
|
|
436
|
+
if (country && !rendered.raw.includes(country)) {
|
|
437
|
+
skipped++
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const { raw, components } = rendered
|
|
442
|
+
const localeTag = rowISO2 === "US" ? "en-US" : `${rowISO2.toLowerCase()}-${rowISO2}`
|
|
443
|
+
|
|
444
|
+
if (opts.golden) {
|
|
445
|
+
write(JSON.stringify({ raw, components, country: rowISO2 }) + "\n")
|
|
446
|
+
emitted++
|
|
447
|
+
continue
|
|
448
|
+
}
|
|
449
|
+
const canonical: CanonicalRow = {
|
|
450
|
+
raw,
|
|
451
|
+
components,
|
|
452
|
+
country: rowISO2,
|
|
453
|
+
locale: localeTag,
|
|
454
|
+
source,
|
|
455
|
+
source_id: stableSourceID(source, components),
|
|
456
|
+
corpus_version: "0.4.0",
|
|
457
|
+
license: "OpenAddresses multi-locale skeletons + injected ISO-3166 country surface forms (codex)",
|
|
458
|
+
}
|
|
459
|
+
const aligned = alignRow(canonical)
|
|
460
|
+
|
|
461
|
+
if (aligned.kind !== "labeled" || !aligned.row) {
|
|
462
|
+
skipped++
|
|
463
|
+
continue
|
|
464
|
+
}
|
|
465
|
+
write(JSON.stringify({ ...aligned.row, synth_method: "country", synth_base_id: null }) + "\n")
|
|
466
|
+
emitted++
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return { emitted, skipped }
|
|
470
|
+
},
|
|
471
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `cz-pcfirst-preposition` — the Czech pc-first prepositional-locality shard, fourth orthography
|
|
7
|
+
* of the #901 leading-name-boundary family. The #897 close-out read all 8 residual CZ rows as one
|
|
8
|
+
* class: a LEADING postcode mis-assigned as house_number while the multi-word "nad/pod/u X"
|
|
9
|
+
* locality shatters ("51244 Rokytnice nad Jizerou, Dolní Rokytnice 111" → street
|
|
10
|
+
* 'RokytnicenadJizerou' + house_number '51244'). That leading-5-digit confusion is the
|
|
11
|
+
* anchor-pollution class whose decode-time OVERRIDE was correctly killed in #723 — this shard is
|
|
12
|
+
* the model-first fix as DATA: real prepositional localities in the order that breaks, so the
|
|
13
|
+
* model learns that a leading postcode before a multi-word name is a postcode. pc-first leads the
|
|
14
|
+
* cycle (the lesson); canonical and city-first keep the polarity balanced (the v1.9.9 lesson).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { alignAndWrite, makeMulberry32, readTuples, type ShardRecipe, shardSourceID } from "./scaffold.ts"
|
|
18
|
+
|
|
19
|
+
export const czPcFirstPrepositionRecipe: ShardRecipe = {
|
|
20
|
+
name: "cz-pcfirst-preposition",
|
|
21
|
+
description:
|
|
22
|
+
"CZ pc-first + prepositional locality (#901 family): '«pc» «city nad X», «st» «n»' — the #723 class as data",
|
|
23
|
+
mode: "tuples",
|
|
24
|
+
async run(opts, write) {
|
|
25
|
+
makeMulberry32(opts.seed)
|
|
26
|
+
let read = 0
|
|
27
|
+
let emitted = 0
|
|
28
|
+
let skipped = 0
|
|
29
|
+
|
|
30
|
+
for await (const t of readTuples(opts.input!)) {
|
|
31
|
+
read++
|
|
32
|
+
const street = String(t.street ?? "").trim()
|
|
33
|
+
const city = String(t.locality ?? "").trim()
|
|
34
|
+
const number = String(t.number ?? "").trim()
|
|
35
|
+
const postcode = String(t.postcode ?? "").trim()
|
|
36
|
+
|
|
37
|
+
if (!street || !city || !number || !postcode) {
|
|
38
|
+
skipped++
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
const order = read % 3
|
|
42
|
+
let raw: string
|
|
43
|
+
const components: Record<string, string> = {
|
|
44
|
+
street,
|
|
45
|
+
house_number: number,
|
|
46
|
+
postcode,
|
|
47
|
+
locality: city,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (order === 0) {
|
|
51
|
+
raw = `${postcode} ${city}, ${street} ${number}`
|
|
52
|
+
} else if (order === 1) {
|
|
53
|
+
raw = `${street} ${number}, ${postcode} ${city}`
|
|
54
|
+
} else {
|
|
55
|
+
raw = `${city}, ${postcode}, ${street} ${number}`
|
|
56
|
+
}
|
|
57
|
+
const source_id = shardSourceID("synth-cz-pcfirst-preposition", {
|
|
58
|
+
...components,
|
|
59
|
+
o: String(order),
|
|
60
|
+
v: String(read),
|
|
61
|
+
})
|
|
62
|
+
const canonical = {
|
|
63
|
+
raw,
|
|
64
|
+
components,
|
|
65
|
+
country: "CZ",
|
|
66
|
+
locale: "cs-CZ",
|
|
67
|
+
source: "synth-cz-pcfirst-preposition",
|
|
68
|
+
source_id,
|
|
69
|
+
corpus_version: "0.10.0",
|
|
70
|
+
license:
|
|
71
|
+
"Synthetic — cz-pcfirst-preposition; (street, number, postcode, city) from OpenAddresses CZ (per-source attribution in the model card)",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (alignAndWrite(write, canonical, "cz-pcfirst-preposition")) {
|
|
75
|
+
emitted++
|
|
76
|
+
} else {
|
|
77
|
+
skipped++
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { read, emitted, skipped }
|
|
82
|
+
},
|
|
83
|
+
}
|