@mailwoman/ban 5.7.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/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @mailwoman/ban
2
+
3
+ Base Adresse Nationale (France) rooftop ingestion. This package reads the open BAN CSV dumps
4
+ (`adresses-<dept>.csv`, adresse.data.gouv.fr) and builds the **national FR address-point shard** on the
5
+ same situs schema the US rooftop tier already uses — so the existing `AddressPointSqliteLookup` reads it
6
+ with zero changes, and the resolver gains rooftop precision across France from the authoritative
7
+ government register (26M addresses) instead of the sparse community fallback (OSM-FR, ~1.1M points).
8
+
9
+ It is the French counterpart of the 50-state US situs layer (#1012). It closes the measured FR rooftop
10
+ gap: commune resolution was already ~99% @25 km, but @1 km sat at ~37% and was _flat_ from clean to messy
11
+ input — the flatness is the tell of a coverage ceiling, not a parse problem. BAN is the coverage.
12
+
13
+ ## The licensing boundary
14
+
15
+ Unlike the ODbL OpenStreetMap tier, BAN is published under the **Licence Ouverte / Open Licence 2.0
16
+ (Etalab)** — attribution only, **no share-alike**. So the built shard ships under the same terms as the
17
+ permissive Mailwoman core (Who's On First, Overture, OpenAddresses, GeoNames); there is no lawyer sign-off
18
+ gate. The one standing obligation is attribution:
19
+
20
+ - **This package is code, and code only.** It contains no BAN bytes.
21
+ - **Attribution rides on any result resolved through a BAN point.** The `source` on every BAN point is
22
+ `ban:fr`, and the resolver should surface
23
+ _"© les contributeurs de la Base Adresse Nationale (adresse.data.gouv.fr)"_.
24
+
25
+ ## Building the shard
26
+
27
+ BAN's per-département dumps land under `$MAILWOMAN_DATA_ROOT/…/ban/` (or the corpus source dir). The build
28
+ streams them — no external CLI, no DuckDB — so it is dependency-light and OOM-safe on the 26M-row national
29
+ set.
30
+
31
+ ```bash
32
+ # 1. Download the per-département dumps (or the national adresses-france.csv.gz):
33
+ # https://adresse.data.gouv.fr/data/ban/adresses/latest/csv/adresses-<dept>.csv.gz
34
+ # → $MAILWOMAN_DATA_ROOT/ban/sources/ (or reuse an existing corpus/sources/ban)
35
+
36
+ # 2. Build the national shard (writes $MAILWOMAN_DATA_ROOT/ban/address-points-fr.db, sealed 0444):
37
+ node ban/out/scripts/build-address-point-shard.js \
38
+ --csv-dir $MAILWOMAN_DATA_ROOT/corpus/sources/ban --release 2026-05-18
39
+
40
+ # Validate on a few départements first (transient; skips the provenance rewrite):
41
+ node ban/out/scripts/build-address-point-shard.js --depts 48,2A,05 --out /tmp/ban-sample.db
42
+ ```
43
+
44
+ The build records provenance (source URL, license, release, row count, md5) in `ban/ATTRIBUTION.json` at
45
+ creation, seals the artifact read-only, and swaps it into place atomically — it is a new, purely-additive
46
+ file and never touches the OSM shard beside it.
47
+
48
+ ## The resolution tier
49
+
50
+ `BANShardProvider.for(country)` is wired into `GeocodeDeps.nationalShards`, consulted **ahead of** the OSM
51
+ `osmShards` tier (a national authoritative register outranks the community fallback) and only for a non-US
52
+ parse. BAN rows carry their own postcode + commune, so the lookup keys on the scoped
53
+ (`postcode` → `locality`) probes; no bbox fall-through is needed. Interpolation for house numbers BAN
54
+ doesn't carry is not built yet — the exact-point tier is the whole win here (BAN's density is the point).
package/out/index.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/ban` — Base Adresse Nationale (France) rooftop address-point ingestion. The FR
7
+ * counterpart of the 50-state US situs layer (#1012): the national government address register (26M
8
+ * addresses) that closes the rooftop gap OSM-FR (~1.1M points) can't. PERMISSIVE CODE ONLY — this
9
+ * workspace contains no BAN data bytes. It reads the open `adresses-<dept>.csv` dumps
10
+ * (adresse.data.gouv.fr, Licence Ouverte/Etalab) and writes a national FR shard on the SHARED situs
11
+ * schema (`@mailwoman/resolver-wof-sqlite/address-point-schema`), so the existing
12
+ * `AddressPointSqliteLookup` reads it with zero changes. See `./sdk` for the ingestion surface and
13
+ * `./scripts/build-address-point-shard` for the build CLI.
14
+ */
15
+ export * from "./sdk/index.js";
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,cAAc,gBAAgB,CAAA"}
package/out/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/ban` — Base Adresse Nationale (France) rooftop address-point ingestion. The FR
7
+ * counterpart of the 50-state US situs layer (#1012): the national government address register (26M
8
+ * addresses) that closes the rooftop gap OSM-FR (~1.1M points) can't. PERMISSIVE CODE ONLY — this
9
+ * workspace contains no BAN data bytes. It reads the open `adresses-<dept>.csv` dumps
10
+ * (adresse.data.gouv.fr, Licence Ouverte/Etalab) and writes a national FR shard on the SHARED situs
11
+ * schema (`@mailwoman/resolver-wof-sqlite/address-point-schema`), so the existing
12
+ * `AddressPointSqliteLookup` reads it with zero changes. See `./sdk` for the ingestion surface and
13
+ * `./scripts/build-address-point-shard` for the build CLI.
14
+ */
15
+ export * from "./sdk/index.js";
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,cAAc,gBAAgB,CAAA"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Build the national FR ROOFTOP address-point shard from the BAN `adresses-<dept>.csv` dumps
7
+ * (adresse.data.gouv.fr), on the SHARED situs schema (`@mailwoman/resolver-wof-sqlite/address-point-schema`)
8
+ * so the existing `AddressPointSqliteLookup` reads it with zero changes (#1012). BAN is a structured
9
+ * government register — every row carries `numero`/`nom_voie`/`code_postal`/`nom_commune`/`lon`/`lat`,
10
+ * so there is no OSM-style association gap: we write the exact source coordinate for every valid row.
11
+ *
12
+ * The `rep` (repetition: bis/ter/…) is folded into the house-number key (`"8 bis"`), so a parsed
13
+ * `"8 bis Rue X"` matches; plain-number rows are keyed on the bare number, unchanged. Keying uses THE
14
+ * shared FR normalizer (`normalizeStreetForKeyLocale(street, "fr")`) — the identical function the
15
+ * lookup tier applies at query time, so build-side and probe-side can't drift.
16
+ *
17
+ * Build discipline (house rules): stream → positional prepared INSERT (batched) → indexes → ANALYZE →
18
+ * atomic swap into place → SEAL 0444 → record md5 + provenance in `ban/ATTRIBUTION.json`. The output
19
+ * is a NEW, purely-additive artifact (`ban/address-points-fr.db`); it never touches the OSM shard.
20
+ *
21
+ * BAN is published under the Licence Ouverte / Etalab 2.0 (attribution, NO share-alike), so the built
22
+ * shard ships under the same terms as the permissive core — no ODbL lawyer gate. `source = "ban:fr"`.
23
+ *
24
+ * Usage:
25
+ * node ban/out/scripts/build-address-point-shard.js \
26
+ * --csv-dir $MAILWOMAN_DATA_ROOT/corpus/sources/ban --release 2026-05-18
27
+ * # validate on a few départements first:
28
+ * node ban/out/scripts/build-address-point-shard.js --depts 48,2A,05 --out /tmp/ban-sample.db
29
+ */
30
+ export {};
31
+ //# sourceMappingURL=build-address-point-shard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-address-point-shard.d.ts","sourceRoot":"","sources":["../../scripts/build-address-point-shard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG"}
@@ -0,0 +1,201 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Build the national FR ROOFTOP address-point shard from the BAN `adresses-<dept>.csv` dumps
7
+ * (adresse.data.gouv.fr), on the SHARED situs schema (`@mailwoman/resolver-wof-sqlite/address-point-schema`)
8
+ * so the existing `AddressPointSqliteLookup` reads it with zero changes (#1012). BAN is a structured
9
+ * government register — every row carries `numero`/`nom_voie`/`code_postal`/`nom_commune`/`lon`/`lat`,
10
+ * so there is no OSM-style association gap: we write the exact source coordinate for every valid row.
11
+ *
12
+ * The `rep` (repetition: bis/ter/…) is folded into the house-number key (`"8 bis"`), so a parsed
13
+ * `"8 bis Rue X"` matches; plain-number rows are keyed on the bare number, unchanged. Keying uses THE
14
+ * shared FR normalizer (`normalizeStreetForKeyLocale(street, "fr")`) — the identical function the
15
+ * lookup tier applies at query time, so build-side and probe-side can't drift.
16
+ *
17
+ * Build discipline (house rules): stream → positional prepared INSERT (batched) → indexes → ANALYZE →
18
+ * atomic swap into place → SEAL 0444 → record md5 + provenance in `ban/ATTRIBUTION.json`. The output
19
+ * is a NEW, purely-additive artifact (`ban/address-points-fr.db`); it never touches the OSM shard.
20
+ *
21
+ * BAN is published under the Licence Ouverte / Etalab 2.0 (attribution, NO share-alike), so the built
22
+ * shard ships under the same terms as the permissive core — no ODbL lawyer gate. `source = "ban:fr"`.
23
+ *
24
+ * Usage:
25
+ * node ban/out/scripts/build-address-point-shard.js \
26
+ * --csv-dir $MAILWOMAN_DATA_ROOT/corpus/sources/ban --release 2026-05-18
27
+ * # validate on a few départements first:
28
+ * node ban/out/scripts/build-address-point-shard.js --depts 48,2A,05 --out /tmp/ban-sample.db
29
+ */
30
+ import { createHash } from "node:crypto";
31
+ import { createReadStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
32
+ import { dirname } from "node:path";
33
+ import { DatabaseSync } from "node:sqlite";
34
+ import { parseArgs } from "node:util";
35
+ import { DatabaseClient } from "@mailwoman/core/kysley/client";
36
+ import { dataRootPath, sealDatabase } from "@mailwoman/core/utils";
37
+ import { ADDRESS_POINT_COLUMNS, createAddressPointIndexes, createAddressPointTable, } from "@mailwoman/resolver-wof-sqlite/address-point-schema";
38
+ import { canonicalizeRouteKey, normalizeLocalityForKey, normalizeStreetForKeyLocale, } from "@mailwoman/resolver-wof-sqlite/street-normalize";
39
+ import { extractBANAddrPoints } from "../sdk/extract.js";
40
+ import { BAN_ATTRIBUTION, BAN_CSV_BASE, BAN_LICENSE } from "../sdk/fetch.js";
41
+ import { streetLocaleForBANCountry } from "../sdk/street-locale.js";
42
+ function parse() {
43
+ const { values } = parseArgs({
44
+ options: {
45
+ country: { type: "string" },
46
+ "csv-dir": { type: "string" },
47
+ release: { type: "string" },
48
+ out: { type: "string" },
49
+ depts: { type: "string" },
50
+ },
51
+ });
52
+ const country = (values.country ?? "fr").toLowerCase();
53
+ // Throws for an unsupported country — fail loud, never key with the wrong normalizer.
54
+ streetLocaleForBANCountry(country);
55
+ const csvDir = values["csv-dir"] ?? dataRootPath("corpus", "sources", "ban");
56
+ if (!existsSync(csvDir))
57
+ throw new Error(`BAN CSV dir not found: ${csvDir}`);
58
+ const release = values.release ?? "2026-05-18";
59
+ const output = values.out ?? dataRootPath("ban", `address-points-${country}.db`);
60
+ const depts = values.depts
61
+ ? values.depts
62
+ .split(",")
63
+ .map((d) => d.trim())
64
+ .filter(Boolean)
65
+ : null;
66
+ return { country, csvDir, release, output, depts };
67
+ }
68
+ /**
69
+ * Enumerate the per-département BAN dumps in `csvDir`, keyed by département code. Excludes the `merged` / `france`
70
+ * aggregates (they duplicate the per-département rows), and prefers an uncompressed `.csv` over a `.csv.gz` when both
71
+ * exist (the same dept, faster read). When `depts` is set, restricts to that list (for a fast validation build).
72
+ */
73
+ function departementFiles(csvDir, depts) {
74
+ const byDept = new Map();
75
+ const wanted = depts ? new Set(depts.map((d) => d.toLowerCase())) : null;
76
+ for (const name of readdirSync(csvDir).sort()) {
77
+ const m = /^adresses-(.+?)\.csv(\.gz)?$/.exec(name);
78
+ if (!m)
79
+ continue;
80
+ const dept = m[1];
81
+ // The aggregate dumps double-count the per-département rows — skip them.
82
+ if (dept === "merged" || dept === "france")
83
+ continue;
84
+ if (wanted && !wanted.has(dept.toLowerCase()))
85
+ continue;
86
+ const path = `${csvDir}/${name}`;
87
+ const existing = byDept.get(dept);
88
+ // Prefer the uncompressed .csv over a .csv.gz for the same dept.
89
+ if (!existing || (existing.endsWith(".gz") && !name.endsWith(".gz"))) {
90
+ byDept.set(dept, path);
91
+ }
92
+ }
93
+ return byDept;
94
+ }
95
+ /** Streaming md5 of a file (never buffer a multi-GB artifact). */
96
+ async function fileMD5(path) {
97
+ const hash = createHash("md5");
98
+ for await (const chunk of createReadStream(path)) {
99
+ hash.update(chunk);
100
+ }
101
+ return hash.digest("hex");
102
+ }
103
+ async function main() {
104
+ const args = parse();
105
+ const locale = streetLocaleForBANCountry(args.country);
106
+ const source = `ban:${args.country}`;
107
+ const files = departementFiles(args.csvDir, args.depts);
108
+ if (files.size === 0)
109
+ throw new Error(`no BAN département dumps found in ${args.csvDir}`);
110
+ const tmp = `${args.output}.building-${process.pid}.db`;
111
+ mkdirSync(dirname(args.output), { recursive: true });
112
+ for (const sfx of ["", "-wal", "-shm"]) {
113
+ rmSync(tmp + sfx, { force: true });
114
+ }
115
+ const out = new DatabaseSync(tmp);
116
+ out.exec("PRAGMA page_size=8192; PRAGMA journal_mode=OFF; PRAGMA synchronous=OFF; PRAGMA cache_size=-2000000;");
117
+ const kdb = new DatabaseClient({ database: out });
118
+ await createAddressPointTable(kdb);
119
+ const insert = out.prepare(`INSERT INTO address_point VALUES (${ADDRESS_POINT_COLUMNS.map(() => "?").join(", ")})`);
120
+ let total = 0;
121
+ let written = 0;
122
+ let noStreet = 0;
123
+ const BATCH = 50_000;
124
+ const deptList = [...files.keys()].sort();
125
+ console.error(`[ban] building ${args.country} rooftop shard from ${files.size} départements in ${args.csvDir}`);
126
+ out.exec("BEGIN");
127
+ for (const dept of deptList) {
128
+ const path = files.get(dept);
129
+ for await (const rec of extractBANAddrPoints(path)) {
130
+ total++;
131
+ const streetNorm = normalizeStreetForKeyLocale(rec.street, locale);
132
+ const numTrim = rec.numero.trim().toLowerCase();
133
+ if (!streetNorm || !numTrim) {
134
+ noStreet++;
135
+ continue;
136
+ }
137
+ // Fold `rep` into the house-number key: "8" + "bis" → "8 bis" (matches a parsed "8 bis Rue X").
138
+ const number = rec.rep ? `${numTrim} ${rec.rep}` : numTrim;
139
+ // Positional, in ADDRESS_POINT_COLUMNS order: street_norm, street_key, number, unit, postcode,
140
+ // locality_norm, street_raw, lat, lon, source, release.
141
+ insert.run(streetNorm, canonicalizeRouteKey(streetNorm), number, null, rec.postcode, rec.city ? normalizeLocalityForKey(rec.city) : null, rec.street, rec.lat, rec.lon, source, args.release);
142
+ written++;
143
+ if (written % BATCH === 0) {
144
+ out.exec("COMMIT");
145
+ out.exec("BEGIN");
146
+ if (written % 2_000_000 === 0) {
147
+ console.error(`[ban] ${written.toLocaleString()} written…`);
148
+ }
149
+ }
150
+ }
151
+ console.error(`[ban] dept ${dept}: ${written.toLocaleString()} cumulative`);
152
+ }
153
+ out.exec("COMMIT");
154
+ console.error(`[ban] indexing…`);
155
+ await createAddressPointIndexes(kdb);
156
+ out.exec("ANALYZE");
157
+ await kdb.destroy();
158
+ // Atomic swap into place (move any prior aside first), then SEAL 0444.
159
+ if (existsSync(args.output)) {
160
+ renameSync(args.output, `${args.output}.prev`);
161
+ }
162
+ for (const sfx of ["-wal", "-shm"]) {
163
+ rmSync(args.output + sfx, { force: true });
164
+ }
165
+ renameSync(tmp, args.output);
166
+ if (existsSync(`${args.output}.prev`)) {
167
+ rmSync(`${args.output}.prev`, { force: true });
168
+ }
169
+ sealDatabase(args.output);
170
+ const md5 = await fileMD5(args.output);
171
+ const bytes = statSync(args.output).size;
172
+ // Provenance manifest — additive, written at creation (house discipline). Only for a FULL national build
173
+ // (the fast --depts validation builds are transient and don't rewrite the record).
174
+ if (!args.depts) {
175
+ const attributionPath = dataRootPath("ban", "ATTRIBUTION.json");
176
+ writeFileSync(attributionPath, JSON.stringify({
177
+ artifact: `address-points-${args.country}.db`,
178
+ source,
179
+ sourceURL: BAN_CSV_BASE,
180
+ license: BAN_LICENSE,
181
+ attribution: BAN_ATTRIBUTION,
182
+ release: args.release,
183
+ departements: deptList.length,
184
+ totalPoints: written,
185
+ bytes,
186
+ md5,
187
+ builtAt: new Date().toISOString(),
188
+ }, null, 2) + "\n");
189
+ console.error(`[ban] wrote ${attributionPath}`);
190
+ }
191
+ console.error(`[ban] DONE ${args.output}\n` +
192
+ ` départements : ${deptList.length}\n` +
193
+ ` total source rows : ${total.toLocaleString()}\n` +
194
+ ` written points : ${written.toLocaleString()}\n` +
195
+ ` skipped (no street/number) : ${noStreet.toLocaleString()}\n` +
196
+ ` bytes : ${bytes.toLocaleString()}\n` +
197
+ ` md5 : ${md5}\n` +
198
+ ` source : ${source} release=${args.release} license=${BAN_LICENSE}`);
199
+ }
200
+ await main();
201
+ //# sourceMappingURL=build-address-point-shard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-address-point-shard.js","sourceRoot":"","sources":["../../scripts/build-address-point-shard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The two contracts the BAN ingest must hold: (1) the extract locates its columns BY NAME off the
7
+ * header (never by fixed position) and drops coordinate-less / streetless rows; (2) the provider is
8
+ * FR-only and keys with the FR street locale — never a silent fold with the wrong rules. Build/probe
9
+ * street-key consistency is covered by `@mailwoman/osm`'s `street-locale.test.ts` (same normalizer).
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=ban.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ban.test.d.ts","sourceRoot":"","sources":["../../sdk/ban.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The two contracts the BAN ingest must hold: (1) the extract locates its columns BY NAME off the
7
+ * header (never by fixed position) and drops coordinate-less / streetless rows; (2) the provider is
8
+ * FR-only and keys with the FR street locale — never a silent fold with the wrong rules. Build/probe
9
+ * street-key consistency is covered by `@mailwoman/osm`'s `street-locale.test.ts` (same normalizer).
10
+ */
11
+ import { mkdtempSync, writeFileSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { expect, test } from "vitest";
15
+ import { extractBANAddrPoints } from "./extract.js";
16
+ import { streetLocaleForBANCountry, supportedBANCountries } from "./street-locale.js";
17
+ const HEADER = "id;id_fantoir;numero;rep;nom_voie;code_postal;code_insee;nom_commune;code_insee_ancienne_commune;nom_ancienne_commune;x;y;lon;lat;type_position;alias;nom_ld;libelle_acheminement;nom_afnor;source_position;source_nom_voie;certification_commune;cad_parcelles";
18
+ function fixtureCSV(rows) {
19
+ const dir = mkdtempSync(join(tmpdir(), "ban-test-"));
20
+ const path = join(dir, "adresses-48.csv");
21
+ writeFileSync(path, [HEADER, ...rows].join("\n") + "\n");
22
+ return path;
23
+ }
24
+ test("extract: yields the full tuple, folds rep to lower-case, skips coordinate-less rows", async () => {
25
+ const path = fixtureCSV([
26
+ "48004_x_1;;6;;Route de Pomaret;48800;48004;Altier;;;766812;6375458;3.840026;44.474983;entrée;;;;;;;1;",
27
+ "48005_x_2;;8;BIS;route de Laval;48310;48005;Albaret-le-Comtal;;;0;0;3.127407;44.877158;entrée;;;;;;;1;",
28
+ "48006_x_3;;9;;Rue Sans Coord;48000;48006;Mende;;;;;;;entrée;;;;;;;1;", // empty lon/lat → skipped
29
+ ]);
30
+ const recs = [];
31
+ for await (const r of extractBANAddrPoints(path)) {
32
+ recs.push(r);
33
+ }
34
+ expect(recs).toHaveLength(2);
35
+ expect(recs[0]).toMatchObject({
36
+ numero: "6",
37
+ rep: null,
38
+ street: "Route de Pomaret",
39
+ postcode: "48800",
40
+ city: "Altier",
41
+ });
42
+ expect(recs[0].lat).toBeCloseTo(44.474983);
43
+ expect(recs[1]).toMatchObject({ numero: "8", rep: "bis" }); // uppercase BIS folded
44
+ });
45
+ test("provider: FR-only, keys with the FR street locale, throws otherwise", () => {
46
+ expect(supportedBANCountries()).toEqual(["fr"]);
47
+ expect(streetLocaleForBANCountry("FR")).toBe("fr");
48
+ expect(() => streetLocaleForBANCountry("de")).toThrow(/No BAN street-normalization locale/);
49
+ });
50
+ //# sourceMappingURL=ban.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ban.test.js","sourceRoot":"","sources":["../../sdk/ban.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAErC,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAErF,MAAM,MAAM,GACX,iQAAiQ,CAAA;AAElQ,SAAS,UAAU,CAAC,IAAc;IACjC,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,WAAW,CAAC,CAAC,CAAA;IACpD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAA;IACzC,aAAa,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAA;IAExD,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,IAAI,CAAC,qFAAqF,EAAE,KAAK,IAAI,EAAE;IACtG,MAAM,IAAI,GAAG,UAAU,CAAC;QACvB,uGAAuG;QACvG,wGAAwG;QACxG,sEAAsE,EAAE,0BAA0B;KAClG,CAAC,CAAA;IAEF,MAAM,IAAI,GAAG,EAAE,CAAA;IAEf,IAAI,KAAK,EAAE,MAAM,CAAC,IAAI,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC;QAClD,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACb,CAAC;IAED,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;IAC5B,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC;QAC7B,MAAM,EAAE,GAAG;QACX,GAAG,EAAE,IAAI;QACT,MAAM,EAAE,kBAAkB;QAC1B,QAAQ,EAAE,OAAO;QACjB,IAAI,EAAE,QAAQ;KACd,CAAC,CAAA;IACF,MAAM,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,GAAG,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,CAAA;IAC3C,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAA,CAAC,uBAAuB;AACnF,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,qEAAqE,EAAE,GAAG,EAAE;IAChF,MAAM,CAAC,qBAAqB,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IAC/C,MAAM,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAClD,MAAM,CAAC,GAAG,EAAE,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAA;AAC5F,CAAC,CAAC,CAAA"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Stream rooftop address records out of a BAN `adresses-<dept>.csv` dump (adresse.data.gouv.fr). The
7
+ * dump is a `;`-delimited, header-first CSV in which EVERY row already carries the full tuple —
8
+ * `numero`, `rep`, `nom_voie`, `code_postal`, `nom_commune`, `lon`/`lat` — so there is no OSM-style
9
+ * "association gap" (a point with no street): BAN is a structured government register, not a
10
+ * community tag soup. We stream line-by-line (the national set is 26M rows / ~5 GB uncompressed);
11
+ * `.csv.gz` inputs are transparently gunzipped.
12
+ *
13
+ * The columns are located BY NAME off the header row (never by fixed position) so a future BAN
14
+ * schema addition can't silently shift the tuple. Values contain no embedded `;` (the dumps are
15
+ * uniform-arity — verified 23 fields across every département), so a plain split is both correct and
16
+ * fast; a literal `"` inside a field (rare) is kept verbatim, never treated as a CSV quote wrapper.
17
+ */
18
+ /** One BAN address point. Every field but `rep`/`postcode`/`city` is guaranteed present by the source. */
19
+ export interface BANAddrRecord {
20
+ /** `numero` — the house number (numeric in BAN; the `rep` suffix is carried separately). */
21
+ numero: string;
22
+ /** `rep` — the repetition indicator (`bis`, `ter`, `quater`, `a`, `b`, …), or null when absent. */
23
+ rep: string | null;
24
+ /** `nom_voie` — the street/voie name, already in full form ("Route de …", "Rue du …"). */
25
+ street: string;
26
+ /** `code_postal` — the 5-digit postcode (nullable: a handful of lieu-dit rows omit it). */
27
+ postcode: string | null;
28
+ /** `nom_commune` — the commune (locality) name. */
29
+ city: string | null;
30
+ lon: number;
31
+ lat: number;
32
+ }
33
+ /**
34
+ * Stream every address point from one BAN département dump, geometry taken straight from the source `lon`/`lat`
35
+ * (WGS84). Rows with a non-finite coordinate or an empty `nom_voie`/`numero` are skipped (yield-side filtering is the
36
+ * caller's job for anything finer). The `rep` suffix is normalised to lower-case or null.
37
+ */
38
+ export declare function extractBANAddrPoints(csvPath: string): AsyncGenerator<BANAddrRecord>;
39
+ //# sourceMappingURL=extract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../sdk/extract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH,0GAA0G;AAC1G,MAAM,WAAW,aAAa;IAC7B,4FAA4F;IAC5F,MAAM,EAAE,MAAM,CAAA;IACd,mGAAmG;IACnG,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,0FAA0F;IAC1F,MAAM,EAAE,MAAM,CAAA;IACd,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mDAAmD;IACnD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;CACX;AA6BD;;;;GAIG;AACH,wBAAuB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAAC,aAAa,CAAC,CAyC1F"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Stream rooftop address records out of a BAN `adresses-<dept>.csv` dump (adresse.data.gouv.fr). The
7
+ * dump is a `;`-delimited, header-first CSV in which EVERY row already carries the full tuple —
8
+ * `numero`, `rep`, `nom_voie`, `code_postal`, `nom_commune`, `lon`/`lat` — so there is no OSM-style
9
+ * "association gap" (a point with no street): BAN is a structured government register, not a
10
+ * community tag soup. We stream line-by-line (the national set is 26M rows / ~5 GB uncompressed);
11
+ * `.csv.gz` inputs are transparently gunzipped.
12
+ *
13
+ * The columns are located BY NAME off the header row (never by fixed position) so a future BAN
14
+ * schema addition can't silently shift the tuple. Values contain no embedded `;` (the dumps are
15
+ * uniform-arity — verified 23 fields across every département), so a plain split is both correct and
16
+ * fast; a literal `"` inside a field (rare) is kept verbatim, never treated as a CSV quote wrapper.
17
+ */
18
+ import { createReadStream } from "node:fs";
19
+ import { createGunzip } from "node:zlib";
20
+ import { TextSpliterator } from "spliterator";
21
+ /** The BAN CSV columns this ingest reads — resolved to indices off the header row. */
22
+ const REQUIRED_COLUMNS = ["numero", "rep", "nom_voie", "code_postal", "nom_commune", "lon", "lat"];
23
+ /** Map the header line to the indices of the columns we read; throws if the dump is missing a required column. */
24
+ function headerIndices(header) {
25
+ const cols = header.split(";").map((c) => c.trim());
26
+ const idx = {};
27
+ for (const name of REQUIRED_COLUMNS) {
28
+ const at = cols.indexOf(name);
29
+ if (at < 0)
30
+ throw new Error(`BAN CSV header is missing required column "${name}" (got: ${cols.join(", ")})`);
31
+ idx[name] = at;
32
+ }
33
+ return idx;
34
+ }
35
+ /** A readable stream over `csvPath`, transparently gunzipping a `.csv.gz` input. */
36
+ function openCSV(csvPath) {
37
+ const raw = createReadStream(csvPath);
38
+ return csvPath.endsWith(".gz") ? raw.pipe(createGunzip()) : raw;
39
+ }
40
+ /**
41
+ * Stream every address point from one BAN département dump, geometry taken straight from the source `lon`/`lat`
42
+ * (WGS84). Rows with a non-finite coordinate or an empty `nom_voie`/`numero` are skipped (yield-side filtering is the
43
+ * caller's job for anything finer). The `rep` suffix is normalised to lower-case or null.
44
+ */
45
+ export async function* extractBANAddrPoints(csvPath) {
46
+ let idx = null;
47
+ for await (const line of TextSpliterator.fromAsync(openCSV(csvPath))) {
48
+ const row = typeof line === "string" ? line : String(line);
49
+ if (!row)
50
+ continue;
51
+ if (!idx) {
52
+ idx = headerIndices(row);
53
+ continue;
54
+ }
55
+ const cols = row.split(";");
56
+ const numero = cols[idx.numero]?.trim();
57
+ const street = cols[idx.nom_voie]?.trim();
58
+ if (!numero || !street)
59
+ continue;
60
+ // Guard the empty-string trap: `Number("")` is 0 (finite), which would write a bogus (0,0) point —
61
+ // so require a non-empty coord string BEFORE parsing, then the finite check catches garbage.
62
+ const lonStr = cols[idx.lon]?.trim();
63
+ const latStr = cols[idx.lat]?.trim();
64
+ if (!lonStr || !latStr)
65
+ continue;
66
+ const lon = Number(lonStr);
67
+ const lat = Number(latStr);
68
+ if (!Number.isFinite(lon) || !Number.isFinite(lat))
69
+ continue;
70
+ const rep = cols[idx.rep]?.trim();
71
+ const postcode = cols[idx.code_postal]?.trim();
72
+ const city = cols[idx.nom_commune]?.trim();
73
+ yield {
74
+ numero,
75
+ rep: rep ? rep.toLowerCase() : null,
76
+ street,
77
+ postcode: postcode || null,
78
+ city: city || null,
79
+ lon,
80
+ lat,
81
+ };
82
+ }
83
+ }
84
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.js","sourceRoot":"","sources":["../../sdk/extract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * BAN dump URLs + provenance constants. The Base Adresse Nationale publishes per-département and
7
+ * national CSV exports at adresse.data.gouv.fr under the Licence Ouverte / Etalab 2.0. We pull the
8
+ * per-département dumps (`adresses-<dept>.csv.gz`) — the ecosystem's default shard unit — and assemble
9
+ * the national FR shard from them. These constants keep the build reproducible + the provenance
10
+ * record honest (the source URL, the license, the required attribution string).
11
+ */
12
+ /** The BAN "latest" CSV export root. */
13
+ export declare const BAN_CSV_BASE = "https://adresse.data.gouv.fr/data/ban/adresses/latest/csv";
14
+ /** Licence Ouverte / Etalab 2.0 — attribution, NO share-alike (unlike ODbL). */
15
+ export declare const BAN_LICENSE = "Licence Ouverte / Open Licence 2.0 (Etalab)";
16
+ /** The attribution string a result resolved through a BAN point must surface. */
17
+ export declare const BAN_ATTRIBUTION = "\u00A9 les contributeurs de la Base Adresse Nationale (adresse.data.gouv.fr)";
18
+ /**
19
+ * The download URL of one département's BAN dump. `dept` is the INSEE département code — `01`…`95`, the Corsica codes
20
+ * `2A`/`2B`, or an overseas code (`971`…`976`). Pass the code exactly as BAN names the file.
21
+ */
22
+ export declare function banDepartementURL(dept: string): string;
23
+ /** The download URL of the whole-country BAN dump (`adresses-france.csv.gz`). */
24
+ export declare function banNationalURL(): string;
25
+ //# sourceMappingURL=fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../sdk/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,wCAAwC;AACxC,eAAO,MAAM,YAAY,8DAA8D,CAAA;AAEvF,gFAAgF;AAChF,eAAO,MAAM,WAAW,gDAAgD,CAAA;AAExE,iFAAiF;AACjF,eAAO,MAAM,eAAe,iFAA4E,CAAA;AAExG;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,iFAAiF;AACjF,wBAAgB,cAAc,IAAI,MAAM,CAEvC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * BAN dump URLs + provenance constants. The Base Adresse Nationale publishes per-département and
7
+ * national CSV exports at adresse.data.gouv.fr under the Licence Ouverte / Etalab 2.0. We pull the
8
+ * per-département dumps (`adresses-<dept>.csv.gz`) — the ecosystem's default shard unit — and assemble
9
+ * the national FR shard from them. These constants keep the build reproducible + the provenance
10
+ * record honest (the source URL, the license, the required attribution string).
11
+ */
12
+ /** The BAN "latest" CSV export root. */
13
+ export const BAN_CSV_BASE = "https://adresse.data.gouv.fr/data/ban/adresses/latest/csv";
14
+ /** Licence Ouverte / Etalab 2.0 — attribution, NO share-alike (unlike ODbL). */
15
+ export const BAN_LICENSE = "Licence Ouverte / Open Licence 2.0 (Etalab)";
16
+ /** The attribution string a result resolved through a BAN point must surface. */
17
+ export const BAN_ATTRIBUTION = "© les contributeurs de la Base Adresse Nationale (adresse.data.gouv.fr)";
18
+ /**
19
+ * The download URL of one département's BAN dump. `dept` is the INSEE département code — `01`…`95`, the Corsica codes
20
+ * `2A`/`2B`, or an overseas code (`971`…`976`). Pass the code exactly as BAN names the file.
21
+ */
22
+ export function banDepartementURL(dept) {
23
+ return `${BAN_CSV_BASE}/adresses-${dept}.csv.gz`;
24
+ }
25
+ /** The download URL of the whole-country BAN dump (`adresses-france.csv.gz`). */
26
+ export function banNationalURL() {
27
+ return `${BAN_CSV_BASE}/adresses-france.csv.gz`;
28
+ }
29
+ //# sourceMappingURL=fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../sdk/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,wCAAwC;AACxC,MAAM,CAAC,MAAM,YAAY,GAAG,2DAA2D,CAAA;AAEvF,gFAAgF;AAChF,MAAM,CAAC,MAAM,WAAW,GAAG,6CAA6C,CAAA;AAExE,iFAAiF;AACjF,MAAM,CAAC,MAAM,eAAe,GAAG,yEAAyE,CAAA;AAExG;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC7C,OAAO,GAAG,YAAY,aAAa,IAAI,SAAS,CAAA;AACjD,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,cAAc;IAC7B,OAAO,GAAG,YAAY,yBAAyB,CAAA;AAChD,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/ban` SDK — the Base Adresse Nationale ingestion surface. PERMISSIVE CODE ONLY: this
7
+ * workspace contains no BAN data bytes. It reads the open `adresses-<dept>.csv` dumps
8
+ * (adresse.data.gouv.fr) and writes a national FR rooftop address-point shard on the SHARED situs
9
+ * schema (`@mailwoman/resolver-wof-sqlite/address-point-schema`). BAN is published under the Licence
10
+ * Ouverte / Etalab (attribution, NO share-alike), so — unlike the ODbL OSM tier — the built shard
11
+ * ships under the same terms as the permissive core; only the per-row attribution obligation rides
12
+ * on it. See `ban/README.md` for the licensing boundary.
13
+ */
14
+ export * from "./fetch.js";
15
+ export * from "./extract.js";
16
+ export * from "./street-locale.js";
17
+ export * from "./shard-provider.js";
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../sdk/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/ban` SDK — the Base Adresse Nationale ingestion surface. PERMISSIVE CODE ONLY: this
7
+ * workspace contains no BAN data bytes. It reads the open `adresses-<dept>.csv` dumps
8
+ * (adresse.data.gouv.fr) and writes a national FR rooftop address-point shard on the SHARED situs
9
+ * schema (`@mailwoman/resolver-wof-sqlite/address-point-schema`). BAN is published under the Licence
10
+ * Ouverte / Etalab (attribution, NO share-alike), so — unlike the ODbL OSM tier — the built shard
11
+ * ships under the same terms as the permissive core; only the per-row attribution obligation rides
12
+ * on it. See `ban/README.md` for the licensing boundary.
13
+ */
14
+ export * from "./fetch.js";
15
+ export * from "./extract.js";
16
+ export * from "./street-locale.js";
17
+ export * from "./shard-provider.js";
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../sdk/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The BAN rooftop shard provider — the injection point the geocode cascade consults for the national
7
+ * open-register precision tier (#1012), AHEAD of the community OSM tier. Given a data root, it opens
8
+ * `ban/address-points-<cc>.db` with the country's street-normalization locale (so probe-side keying
9
+ * matches the shard the builder wrote) and caches the open handle per country. Wire its bound `for`
10
+ * into `GeocodeDeps.nationalShards`.
11
+ *
12
+ * BAN is a French national register, so the registry is deliberately FR-only today; the shape
13
+ * generalises to any other national open register (the coverage story, one country at a time).
14
+ */
15
+ import { AddressPointSqliteLookup } from "@mailwoman/resolver-wof-sqlite";
16
+ /** What the cascade needs from a BAN shard — structurally a subset of mailwoman's `StateShards`. */
17
+ export interface BANShards {
18
+ addressPoints?: AddressPointSqliteLookup;
19
+ }
20
+ /**
21
+ * Opens + caches per-country BAN rooftop lookups. A non-US geocode consults `for(country)`; the first hit for a country
22
+ * opens its shard (with the matching street locale) once, subsequent calls reuse it.
23
+ */
24
+ export declare class BANShardProvider {
25
+ #private;
26
+ constructor(dataRoot: string);
27
+ /** Resolve the BAN shards for an ISO-3166 alpha-2 country, or `{}` when none is shipped/registered. */
28
+ readonly for: (country: string) => BANShards;
29
+ close(): void;
30
+ }
31
+ //# sourceMappingURL=shard-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shard-provider.d.ts","sourceRoot":"","sources":["../../sdk/shard-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAA;AAIzE,oGAAoG;AACpG,MAAM,WAAW,SAAS;IACzB,aAAa,CAAC,EAAE,wBAAwB,CAAA;CACxC;AAED;;;GAGG;AACH,qBAAa,gBAAgB;;gBAIhB,QAAQ,EAAE,MAAM;IAQ5B,uGAAuG;IACvG,QAAQ,CAAC,GAAG,GAAI,SAAS,MAAM,KAAG,SAAS,CAmB1C;IAED,KAAK,IAAI,IAAI;CAMb"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The BAN rooftop shard provider — the injection point the geocode cascade consults for the national
7
+ * open-register precision tier (#1012), AHEAD of the community OSM tier. Given a data root, it opens
8
+ * `ban/address-points-<cc>.db` with the country's street-normalization locale (so probe-side keying
9
+ * matches the shard the builder wrote) and caches the open handle per country. Wire its bound `for`
10
+ * into `GeocodeDeps.nationalShards`.
11
+ *
12
+ * BAN is a French national register, so the registry is deliberately FR-only today; the shape
13
+ * generalises to any other national open register (the coverage story, one country at a time).
14
+ */
15
+ import { existsSync } from "node:fs";
16
+ import { AddressPointSqliteLookup } from "@mailwoman/resolver-wof-sqlite";
17
+ import { streetLocaleForBANCountry, supportedBANCountries } from "./street-locale.js";
18
+ /**
19
+ * Opens + caches per-country BAN rooftop lookups. A non-US geocode consults `for(country)`; the first hit for a country
20
+ * opens its shard (with the matching street locale) once, subsequent calls reuse it.
21
+ */
22
+ export class BANShardProvider {
23
+ #dataRoot;
24
+ #cache = new Map();
25
+ constructor(dataRoot) {
26
+ this.#dataRoot = dataRoot;
27
+ }
28
+ #shardPath(countryCode) {
29
+ return `${this.#dataRoot}/ban/address-points-${countryCode}.db`;
30
+ }
31
+ /** Resolve the BAN shards for an ISO-3166 alpha-2 country, or `{}` when none is shipped/registered. */
32
+ for = (country) => {
33
+ const cc = country.toLowerCase();
34
+ const cached = this.#cache.get(cc);
35
+ if (cached)
36
+ return cached;
37
+ let entry = {};
38
+ // Only countries with a registered street locale AND an on-disk shard — never key with the wrong rules.
39
+ if (supportedBANCountries().includes(cc)) {
40
+ const path = this.#shardPath(cc);
41
+ if (existsSync(path)) {
42
+ entry = { addressPoints: new AddressPointSqliteLookup(path, { streetLocale: streetLocaleForBANCountry(cc) }) };
43
+ }
44
+ }
45
+ this.#cache.set(cc, entry);
46
+ return entry;
47
+ };
48
+ close() {
49
+ for (const entry of this.#cache.values()) {
50
+ entry.addressPoints?.close();
51
+ }
52
+ this.#cache.clear();
53
+ }
54
+ }
55
+ //# sourceMappingURL=shard-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shard-provider.js","sourceRoot":"","sources":["../../sdk/shard-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAEpC,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAA;AAEzE,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAOrF;;;GAGG;AACH,MAAM,OAAO,gBAAgB;IACnB,SAAS,CAAQ;IACjB,MAAM,GAAG,IAAI,GAAG,EAAqB,CAAA;IAE9C,YAAY,QAAgB;QAC3B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAA;IAC1B,CAAC;IAED,UAAU,CAAC,WAAmB;QAC7B,OAAO,GAAG,IAAI,CAAC,SAAS,uBAAuB,WAAW,KAAK,CAAA;IAChE,CAAC;IAED,uGAAuG;IAC9F,GAAG,GAAG,CAAC,OAAe,EAAa,EAAE;QAC7C,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAA;QAChC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAElC,IAAI,MAAM;YAAE,OAAO,MAAM,CAAA;QAEzB,IAAI,KAAK,GAAc,EAAE,CAAA;QAEzB,wGAAwG;QACxG,IAAI,qBAAqB,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;YAEhC,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;gBACtB,KAAK,GAAG,EAAE,aAAa,EAAE,IAAI,wBAAwB,CAAC,IAAI,EAAE,EAAE,YAAY,EAAE,yBAAyB,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAA;YAC/G,CAAC;QACF,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAA;QAE1B,OAAO,KAAK,CAAA;IACb,CAAC,CAAA;IAED,KAAK;QACJ,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;YAC1C,KAAK,CAAC,aAAa,EAAE,KAAK,EAAE,CAAA;QAC7B,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAA;IACpB,CAAC;CACD"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Country → street-normalization-locale routing for the BAN rooftop build. The NORMALIZER itself
7
+ * lives in `@mailwoman/resolver-wof-sqlite/street-normalize` (the one-function discipline — the
8
+ * reader on the resolver side and the builder here must call the identical function). This module
9
+ * only maps an ISO-3166 country code to the locale that selects the right per-locale rules, and
10
+ * re-exports the normalizer so the BAN SDK is a self-contained surface. Kept SEPARATE from the
11
+ * heavy `shard-provider.ts` (which pulls the SQLite lookup) so the pure locale contract is testable
12
+ * without opening a database — mirrors `@mailwoman/osm`'s `street-locale.ts`.
13
+ */
14
+ import { normalizeStreetForKeyLocale, type StreetLocale } from "@mailwoman/resolver-wof-sqlite/street-normalize";
15
+ export { normalizeStreetForKeyLocale, type StreetLocale };
16
+ /**
17
+ * Resolve the street-normalization locale for a BAN country. Throws for an unsupported country rather than silently
18
+ * folding with the wrong rules — a shard built with the wrong normalizer keys every street incorrectly and looks fine
19
+ * until a probe misses. Add the country to {@link BAN_COUNTRY_TO_STREET_LOCALE} (and a branch in
20
+ * `normalizeStreetForKeyLocale`) before building its shard.
21
+ */
22
+ export declare function streetLocaleForBANCountry(countryCode: string): StreetLocale;
23
+ /** The countries with a registered BAN street locale (for CLI validation / provider gating). */
24
+ export declare function supportedBANCountries(): string[];
25
+ //# sourceMappingURL=street-locale.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"street-locale.d.ts","sourceRoot":"","sources":["../../sdk/street-locale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,2BAA2B,EAAE,KAAK,YAAY,EAAE,MAAM,iDAAiD,CAAA;AAEhH,OAAO,EAAE,2BAA2B,EAAE,KAAK,YAAY,EAAE,CAAA;AASzD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,CAW3E;AAED,gGAAgG;AAChG,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAEhD"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Country → street-normalization-locale routing for the BAN rooftop build. The NORMALIZER itself
7
+ * lives in `@mailwoman/resolver-wof-sqlite/street-normalize` (the one-function discipline — the
8
+ * reader on the resolver side and the builder here must call the identical function). This module
9
+ * only maps an ISO-3166 country code to the locale that selects the right per-locale rules, and
10
+ * re-exports the normalizer so the BAN SDK is a self-contained surface. Kept SEPARATE from the
11
+ * heavy `shard-provider.ts` (which pulls the SQLite lookup) so the pure locale contract is testable
12
+ * without opening a database — mirrors `@mailwoman/osm`'s `street-locale.ts`.
13
+ */
14
+ import { normalizeStreetForKeyLocale } from "@mailwoman/resolver-wof-sqlite/street-normalize";
15
+ export { normalizeStreetForKeyLocale };
16
+ /**
17
+ * ISO-3166 alpha-2 (lowercase) → the street-normalization locale a BAN shard was built with. FR-only: BAN is the French
18
+ * national register. Adding a country here means shipping that country's national register on the shared schema AND
19
+ * having a matching branch in `normalizeStreetForKeyLocale` — never a silent fold with the wrong rules.
20
+ */
21
+ const BAN_COUNTRY_TO_STREET_LOCALE = new Map([["fr", "fr"]]);
22
+ /**
23
+ * Resolve the street-normalization locale for a BAN country. Throws for an unsupported country rather than silently
24
+ * folding with the wrong rules — a shard built with the wrong normalizer keys every street incorrectly and looks fine
25
+ * until a probe misses. Add the country to {@link BAN_COUNTRY_TO_STREET_LOCALE} (and a branch in
26
+ * `normalizeStreetForKeyLocale`) before building its shard.
27
+ */
28
+ export function streetLocaleForBANCountry(countryCode) {
29
+ const locale = BAN_COUNTRY_TO_STREET_LOCALE.get(countryCode.toLowerCase());
30
+ if (!locale) {
31
+ throw new Error(`No BAN street-normalization locale registered for country "${countryCode}". ` +
32
+ `Add it to BAN_COUNTRY_TO_STREET_LOCALE and add the matching branch in normalizeStreetForKeyLocale before building its shard.`);
33
+ }
34
+ return locale;
35
+ }
36
+ /** The countries with a registered BAN street locale (for CLI validation / provider gating). */
37
+ export function supportedBANCountries() {
38
+ return [...BAN_COUNTRY_TO_STREET_LOCALE.keys()];
39
+ }
40
+ //# sourceMappingURL=street-locale.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"street-locale.js","sourceRoot":"","sources":["../../sdk/street-locale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,2BAA2B,EAAqB,MAAM,iDAAiD,CAAA;AAEhH,OAAO,EAAE,2BAA2B,EAAqB,CAAA;AAEzD;;;;GAIG;AACH,MAAM,4BAA4B,GAAG,IAAI,GAAG,CAAuB,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;AAElF;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,WAAmB;IAC5D,MAAM,MAAM,GAAG,4BAA4B,CAAC,GAAG,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC,CAAA;IAE1E,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACd,8DAA8D,WAAW,KAAK;YAC7E,8HAA8H,CAC/H,CAAA;IACF,CAAC;IAED,OAAO,MAAM,CAAA;AACd,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,qBAAqB;IACpC,OAAO,CAAC,GAAG,4BAA4B,CAAC,IAAI,EAAE,CAAC,CAAA;AAChD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@mailwoman/ban",
3
+ "version": "5.7.0",
4
+ "description": "Base Adresse Nationale (France) rooftop address-point ingestion — builds the national FR precision shard on the situs address-point schema.",
5
+ "keywords": [
6
+ "address-point",
7
+ "ban",
8
+ "france",
9
+ "geocoding",
10
+ "geospatial"
11
+ ],
12
+ "homepage": "https://github.com/sister-software/mailwoman",
13
+ "bugs": {
14
+ "url": "https://github.com/sister-software/mailwoman/issues"
15
+ },
16
+ "license": "AGPL-3.0-only OR LicenseRef-Commercial",
17
+ "contributors": [
18
+ {
19
+ "name": "Teffen Ellis",
20
+ "email": "teffen@sister.software"
21
+ }
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/sister-software/mailwoman.git",
26
+ "directory": "ban"
27
+ },
28
+ "files": [
29
+ "out/**/*.js",
30
+ "out/**/*.js.map",
31
+ "out/**/*.d.ts",
32
+ "out/**/*.d.ts.map",
33
+ "README.md"
34
+ ],
35
+ "type": "module",
36
+ "exports": {
37
+ "./package.json": "./package.json",
38
+ "./scripts/*": "./out/scripts/*.js",
39
+ "./sdk": "./out/sdk/index.js",
40
+ ".": "./out/index.js"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "test": "vitest run"
47
+ },
48
+ "dependencies": {
49
+ "@mailwoman/core": "5.7.0",
50
+ "@mailwoman/resolver-wof-sqlite": "5.7.0",
51
+ "kysely": "^0.29.2",
52
+ "spliterator": "^3.2.0"
53
+ },
54
+ "engines": {
55
+ "node": ">=24.18.0"
56
+ }
57
+ }