@mailwoman/ban 5.7.0 → 5.9.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/out/scripts/build-street-centroid-shard.d.ts +28 -0
- package/out/scripts/build-street-centroid-shard.d.ts.map +1 -0
- package/out/scripts/build-street-centroid-shard.js +170 -0
- package/out/scripts/build-street-centroid-shard.js.map +1 -0
- package/out/sdk/shard-provider.d.ts +3 -1
- package/out/sdk/shard-provider.d.ts.map +1 -1
- package/out/sdk/shard-provider.js +13 -3
- package/out/sdk/shard-provider.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software.
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Build the DERIVED street-centroid shard (`ban/street-centroids-<cc>.db`, #1042) from the SEALED
|
|
7
|
+
* rooftop address-point shard (`ban/address-points-<cc>.db`, #1012). No new data source: it is a
|
|
8
|
+
* `GROUP BY street` roll-up of the register we already ingested — one row per (street_norm, postcode,
|
|
9
|
+
* commune) carrying the street's CENTROID + bounding-box EXTENT + member-point count. The output feeds
|
|
10
|
+
* `StreetCentroidSqliteLookup`, the street-level tier for a street-only query (a thoroughfare with NO
|
|
11
|
+
* house number) that no address-POINT tier can serve by definition.
|
|
12
|
+
*
|
|
13
|
+
* The commune is the arrondissement-STRIPPED base commune (`stripArrondissement` — BAN names
|
|
14
|
+
* Paris/Lyon/Marseille rows per arrondissement, but a query names the base commune); the aggregation
|
|
15
|
+
* groups on the full `locality_norm` and emits the base, so a rare (street, postcode, base) collision
|
|
16
|
+
* across two arrondissements is merged harmlessly by the reader's weighted aggregate.
|
|
17
|
+
*
|
|
18
|
+
* The SEALED input is opened READ-ONLY and NEVER modified. Build discipline (house rules): aggregate
|
|
19
|
+
* in SQLite → stream via `.iterate()` → positional prepared INSERT (batched) into a staging DB →
|
|
20
|
+
* indexes → ANALYZE → atomic swap into place → SEAL 0444 → record md5 + the derivation provenance in
|
|
21
|
+
* `ban/street-centroids-<cc>.ATTRIBUTION.json`. Purely additive; it never touches the rooftop shard.
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* node ban/out/scripts/build-street-centroid-shard.js # fr, default paths
|
|
25
|
+
* node ban/out/scripts/build-street-centroid-shard.js --country fr --out /tmp/sc-fr.db
|
|
26
|
+
*/
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=build-street-centroid-shard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-street-centroid-shard.d.ts","sourceRoot":"","sources":["../../scripts/build-street-centroid-shard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software.
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Build the DERIVED street-centroid shard (`ban/street-centroids-<cc>.db`, #1042) from the SEALED
|
|
7
|
+
* rooftop address-point shard (`ban/address-points-<cc>.db`, #1012). No new data source: it is a
|
|
8
|
+
* `GROUP BY street` roll-up of the register we already ingested — one row per (street_norm, postcode,
|
|
9
|
+
* commune) carrying the street's CENTROID + bounding-box EXTENT + member-point count. The output feeds
|
|
10
|
+
* `StreetCentroidSqliteLookup`, the street-level tier for a street-only query (a thoroughfare with NO
|
|
11
|
+
* house number) that no address-POINT tier can serve by definition.
|
|
12
|
+
*
|
|
13
|
+
* The commune is the arrondissement-STRIPPED base commune (`stripArrondissement` — BAN names
|
|
14
|
+
* Paris/Lyon/Marseille rows per arrondissement, but a query names the base commune); the aggregation
|
|
15
|
+
* groups on the full `locality_norm` and emits the base, so a rare (street, postcode, base) collision
|
|
16
|
+
* across two arrondissements is merged harmlessly by the reader's weighted aggregate.
|
|
17
|
+
*
|
|
18
|
+
* The SEALED input is opened READ-ONLY and NEVER modified. Build discipline (house rules): aggregate
|
|
19
|
+
* in SQLite → stream via `.iterate()` → positional prepared INSERT (batched) into a staging DB →
|
|
20
|
+
* indexes → ANALYZE → atomic swap into place → SEAL 0444 → record md5 + the derivation provenance in
|
|
21
|
+
* `ban/street-centroids-<cc>.ATTRIBUTION.json`. Purely additive; it never touches the rooftop shard.
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* node ban/out/scripts/build-street-centroid-shard.js # fr, default paths
|
|
25
|
+
* node ban/out/scripts/build-street-centroid-shard.js --country fr --out /tmp/sc-fr.db
|
|
26
|
+
*/
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
28
|
+
import { createReadStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
29
|
+
import { dirname } from "node:path";
|
|
30
|
+
import { DatabaseSync } from "node:sqlite";
|
|
31
|
+
import { parseArgs } from "node:util";
|
|
32
|
+
import { DatabaseClient } from "@mailwoman/core/kysley/client";
|
|
33
|
+
import { dataRootPath, sealDatabase } from "@mailwoman/core/utils";
|
|
34
|
+
import { createStreetCentroidIndexes, createStreetCentroidTable, STREET_CENTROID_COLUMNS, } from "@mailwoman/resolver-wof-sqlite/street-centroid-schema";
|
|
35
|
+
import { stripArrondissement } from "@mailwoman/resolver-wof-sqlite/street-normalize";
|
|
36
|
+
import { BAN_ATTRIBUTION, BAN_CSV_BASE, BAN_LICENSE } from "../sdk/fetch.js";
|
|
37
|
+
import { streetLocaleForBANCountry } from "../sdk/street-locale.js";
|
|
38
|
+
function parse() {
|
|
39
|
+
const { values } = parseArgs({
|
|
40
|
+
options: {
|
|
41
|
+
country: { type: "string" },
|
|
42
|
+
source: { type: "string" },
|
|
43
|
+
release: { type: "string" },
|
|
44
|
+
out: { type: "string" },
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const country = (values.country ?? "fr").toLowerCase();
|
|
48
|
+
// Throws for an unsupported country — fail loud, never derive a tier keyed with the wrong locale rules.
|
|
49
|
+
streetLocaleForBANCountry(country);
|
|
50
|
+
const source = values.source ?? dataRootPath("ban", `address-points-${country}.db`);
|
|
51
|
+
if (!existsSync(source))
|
|
52
|
+
throw new Error(`sealed BAN rooftop shard not found: ${source} (build it via #1012 first)`);
|
|
53
|
+
const release = values.release ?? "2026-05-18";
|
|
54
|
+
const output = values.out ?? dataRootPath("ban", `street-centroids-${country}.db`);
|
|
55
|
+
return { country, source, release, output };
|
|
56
|
+
}
|
|
57
|
+
/** Streaming md5 of a file (never buffer a multi-GB artifact). */
|
|
58
|
+
async function fileMD5(path) {
|
|
59
|
+
const hash = createHash("md5");
|
|
60
|
+
for await (const chunk of createReadStream(path)) {
|
|
61
|
+
hash.update(chunk);
|
|
62
|
+
}
|
|
63
|
+
return hash.digest("hex");
|
|
64
|
+
}
|
|
65
|
+
/** The md5 the #1012 build recorded for the sealed rooftop input, for the derivation provenance chain. */
|
|
66
|
+
function sourceMD5(country) {
|
|
67
|
+
try {
|
|
68
|
+
const rec = JSON.parse(readFileSync(dataRootPath("ban", "ATTRIBUTION.json"), "utf8"));
|
|
69
|
+
return rec.artifact === `address-points-${country}.db` ? (rec.md5 ?? null) : null;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function main() {
|
|
76
|
+
const args = parse();
|
|
77
|
+
const source = `ban:${args.country}`;
|
|
78
|
+
const tmp = `${args.output}.building-${process.pid}.db`;
|
|
79
|
+
mkdirSync(dirname(args.output), { recursive: true });
|
|
80
|
+
for (const sfx of ["", "-wal", "-shm"]) {
|
|
81
|
+
rmSync(tmp + sfx, { force: true });
|
|
82
|
+
}
|
|
83
|
+
// The SEALED input — READ-ONLY, immutable; register the base-commune folder as a scalar SQL function.
|
|
84
|
+
const src = new DatabaseSync(args.source, { readOnly: true });
|
|
85
|
+
src.function("ban_base_commune", { deterministic: true }, (loc) => typeof loc === "string" && loc ? stripArrondissement(loc) : "");
|
|
86
|
+
const out = new DatabaseSync(tmp);
|
|
87
|
+
out.exec("PRAGMA page_size=8192; PRAGMA journal_mode=OFF; PRAGMA synchronous=OFF; PRAGMA cache_size=-1000000;");
|
|
88
|
+
const kdb = new DatabaseClient({ database: out });
|
|
89
|
+
await createStreetCentroidTable(kdb);
|
|
90
|
+
const insert = out.prepare(`INSERT INTO street_centroid VALUES (${STREET_CENTROID_COLUMNS.map(() => "?").join(", ")})`);
|
|
91
|
+
// GROUP BY the sealed rooftop points into per-(street, postcode, commune) roll-ups. AVG(lat/lon) over the group's
|
|
92
|
+
// member points is the exact centroid; MIN/MAX is the extent; COUNT is the weight for the reader's cross-group mean.
|
|
93
|
+
// The base commune is emitted per group (2.2M calls), NOT per source row.
|
|
94
|
+
const agg = src.prepare(`SELECT street_norm,
|
|
95
|
+
postcode,
|
|
96
|
+
ban_base_commune(locality_norm) AS locality_base,
|
|
97
|
+
AVG(lat) AS lat, AVG(lon) AS lon,
|
|
98
|
+
MIN(lat) AS min_lat, MAX(lat) AS max_lat, MIN(lon) AS min_lon, MAX(lon) AS max_lon,
|
|
99
|
+
COUNT(*) AS n,
|
|
100
|
+
MIN(street_raw) AS street_raw
|
|
101
|
+
FROM address_point
|
|
102
|
+
GROUP BY street_norm, postcode, locality_norm`);
|
|
103
|
+
let written = 0;
|
|
104
|
+
const BATCH = 50_000;
|
|
105
|
+
console.error(`[ban] deriving ${args.country} street-centroid tier from ${args.source}`);
|
|
106
|
+
out.exec("BEGIN");
|
|
107
|
+
for (const row of agg.iterate()) {
|
|
108
|
+
// Positional, in STREET_CENTROID_COLUMNS order.
|
|
109
|
+
insert.run(row.street_norm, row.postcode, row.locality_base, row.lat, row.lon, row.min_lat, row.max_lat, row.min_lon, row.max_lon, row.n, row.street_raw, source, args.release);
|
|
110
|
+
written++;
|
|
111
|
+
if (written % BATCH === 0) {
|
|
112
|
+
out.exec("COMMIT");
|
|
113
|
+
out.exec("BEGIN");
|
|
114
|
+
if (written % 500_000 === 0) {
|
|
115
|
+
console.error(`[ban] ${written.toLocaleString()} streets…`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
out.exec("COMMIT");
|
|
120
|
+
src.close();
|
|
121
|
+
console.error(`[ban] indexing…`);
|
|
122
|
+
await createStreetCentroidIndexes(kdb);
|
|
123
|
+
out.exec("ANALYZE");
|
|
124
|
+
await kdb.destroy();
|
|
125
|
+
// Atomic swap into place (move any prior aside first), then SEAL 0444.
|
|
126
|
+
if (existsSync(args.output)) {
|
|
127
|
+
renameSync(args.output, `${args.output}.prev`);
|
|
128
|
+
}
|
|
129
|
+
for (const sfx of ["-wal", "-shm"]) {
|
|
130
|
+
rmSync(args.output + sfx, { force: true });
|
|
131
|
+
}
|
|
132
|
+
renameSync(tmp, args.output);
|
|
133
|
+
if (existsSync(`${args.output}.prev`)) {
|
|
134
|
+
rmSync(`${args.output}.prev`, { force: true });
|
|
135
|
+
}
|
|
136
|
+
sealDatabase(args.output);
|
|
137
|
+
const md5 = await fileMD5(args.output);
|
|
138
|
+
const bytes = statSync(args.output).size;
|
|
139
|
+
const srcMD5 = sourceMD5(args.country);
|
|
140
|
+
// Provenance manifest — additive, written at creation (house discipline). Records the DERIVATION chain: this
|
|
141
|
+
// artifact is derived from the sealed #1012 rooftop shard, itself derived from the BAN release.
|
|
142
|
+
const attributionPath = dataRootPath("ban", `street-centroids-${args.country}.ATTRIBUTION.json`);
|
|
143
|
+
writeFileSync(attributionPath, JSON.stringify({
|
|
144
|
+
artifact: `street-centroids-${args.country}.db`,
|
|
145
|
+
derivedFrom: {
|
|
146
|
+
artifact: `address-points-${args.country}.db`,
|
|
147
|
+
source,
|
|
148
|
+
release: args.release,
|
|
149
|
+
md5: srcMD5,
|
|
150
|
+
note: `derived from ${source} release=${args.release}${srcMD5 ? ` (md5 ${srcMD5.slice(0, 8)})` : ""}`,
|
|
151
|
+
},
|
|
152
|
+
source,
|
|
153
|
+
sourceURL: BAN_CSV_BASE,
|
|
154
|
+
license: BAN_LICENSE,
|
|
155
|
+
attribution: BAN_ATTRIBUTION,
|
|
156
|
+
release: args.release,
|
|
157
|
+
streets: written,
|
|
158
|
+
bytes,
|
|
159
|
+
md5,
|
|
160
|
+
builtAt: new Date().toISOString(),
|
|
161
|
+
}, null, 2) + "\n");
|
|
162
|
+
console.error(`[ban] wrote ${attributionPath}`);
|
|
163
|
+
console.error(`[ban] DONE ${args.output}\n` +
|
|
164
|
+
` streets (rows) : ${written.toLocaleString()}\n` +
|
|
165
|
+
` bytes : ${bytes.toLocaleString()}\n` +
|
|
166
|
+
` md5 : ${md5}\n` +
|
|
167
|
+
` derived from : address-points-${args.country}.db${srcMD5 ? ` (md5 ${srcMD5.slice(0, 8)})` : ""} release=${args.release}`);
|
|
168
|
+
}
|
|
169
|
+
await main();
|
|
170
|
+
//# sourceMappingURL=build-street-centroid-shard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-street-centroid-shard.js","sourceRoot":"","sources":["../../scripts/build-street-centroid-shard.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
* BAN is a French national register, so the registry is deliberately FR-only today; the shape
|
|
13
13
|
* generalises to any other national open register (the coverage story, one country at a time).
|
|
14
14
|
*/
|
|
15
|
-
import { AddressPointSqliteLookup } from "@mailwoman/resolver-wof-sqlite";
|
|
15
|
+
import { AddressPointSqliteLookup, StreetCentroidSqliteLookup } from "@mailwoman/resolver-wof-sqlite";
|
|
16
16
|
/** What the cascade needs from a BAN shard — structurally a subset of mailwoman's `StateShards`. */
|
|
17
17
|
export interface BANShards {
|
|
18
18
|
addressPoints?: AddressPointSqliteLookup;
|
|
19
|
+
/** The #1042 derived street-centroid tier — a `GROUP BY street` roll-up, for a street-only query (no house number). */
|
|
20
|
+
streetCentroids?: StreetCentroidSqliteLookup;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* Opens + caches per-country BAN rooftop lookups. A non-US geocode consults `for(country)`; the first hit for a country
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"shard-provider.d.ts","sourceRoot":"","sources":["../../sdk/shard-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAA;AAIrG,oGAAoG;AACpG,MAAM,WAAW,SAAS;IACzB,aAAa,CAAC,EAAE,wBAAwB,CAAA;IACxC,uHAAuH;IACvH,eAAe,CAAC,EAAE,0BAA0B,CAAA;CAC5C;AAED;;;GAGG;AACH,qBAAa,gBAAgB;;gBAIhB,QAAQ,EAAE,MAAM;IAY5B,uGAAuG;IACvG,QAAQ,CAAC,GAAG,GAAI,SAAS,MAAM,KAAG,SAAS,CA0B1C;IAED,KAAK,IAAI,IAAI;CAOb"}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* generalises to any other national open register (the coverage story, one country at a time).
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync } from "node:fs";
|
|
16
|
-
import { AddressPointSqliteLookup } from "@mailwoman/resolver-wof-sqlite";
|
|
16
|
+
import { AddressPointSqliteLookup, StreetCentroidSqliteLookup } from "@mailwoman/resolver-wof-sqlite";
|
|
17
17
|
import { streetLocaleForBANCountry, supportedBANCountries } from "./street-locale.js";
|
|
18
18
|
/**
|
|
19
19
|
* Opens + caches per-country BAN rooftop lookups. A non-US geocode consults `for(country)`; the first hit for a country
|
|
@@ -28,18 +28,27 @@ export class BANShardProvider {
|
|
|
28
28
|
#shardPath(countryCode) {
|
|
29
29
|
return `${this.#dataRoot}/ban/address-points-${countryCode}.db`;
|
|
30
30
|
}
|
|
31
|
+
#streetCentroidPath(countryCode) {
|
|
32
|
+
return `${this.#dataRoot}/ban/street-centroids-${countryCode}.db`;
|
|
33
|
+
}
|
|
31
34
|
/** Resolve the BAN shards for an ISO-3166 alpha-2 country, or `{}` when none is shipped/registered. */
|
|
32
35
|
for = (country) => {
|
|
33
36
|
const cc = country.toLowerCase();
|
|
34
37
|
const cached = this.#cache.get(cc);
|
|
35
38
|
if (cached)
|
|
36
39
|
return cached;
|
|
37
|
-
|
|
40
|
+
const entry = {};
|
|
38
41
|
// Only countries with a registered street locale AND an on-disk shard — never key with the wrong rules.
|
|
39
42
|
if (supportedBANCountries().includes(cc)) {
|
|
43
|
+
const locale = streetLocaleForBANCountry(cc);
|
|
40
44
|
const path = this.#shardPath(cc);
|
|
41
45
|
if (existsSync(path)) {
|
|
42
|
-
entry =
|
|
46
|
+
entry.addressPoints = new AddressPointSqliteLookup(path, { streetLocale: locale });
|
|
47
|
+
}
|
|
48
|
+
// The #1042 derived street tier — purely additive, opened only when its artifact is on disk.
|
|
49
|
+
const streetPath = this.#streetCentroidPath(cc);
|
|
50
|
+
if (existsSync(streetPath)) {
|
|
51
|
+
entry.streetCentroids = new StreetCentroidSqliteLookup(streetPath, { streetLocale: locale });
|
|
43
52
|
}
|
|
44
53
|
}
|
|
45
54
|
this.#cache.set(cc, entry);
|
|
@@ -48,6 +57,7 @@ export class BANShardProvider {
|
|
|
48
57
|
close() {
|
|
49
58
|
for (const entry of this.#cache.values()) {
|
|
50
59
|
entry.addressPoints?.close();
|
|
60
|
+
entry.streetCentroids?.close();
|
|
51
61
|
}
|
|
52
62
|
this.#cache.clear();
|
|
53
63
|
}
|
|
@@ -1 +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;
|
|
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,0BAA0B,EAAE,MAAM,gCAAgC,CAAA;AAErG,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AASrF;;;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,mBAAmB,CAAC,WAAmB;QACtC,OAAO,GAAG,IAAI,CAAC,SAAS,yBAAyB,WAAW,KAAK,CAAA;IAClE,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,MAAM,KAAK,GAAc,EAAE,CAAA;QAE3B,wGAAwG;QACxG,IAAI,qBAAqB,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;YAC1C,MAAM,MAAM,GAAG,yBAAyB,CAAC,EAAE,CAAC,CAAA;YAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;YAEhC,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;gBACtB,KAAK,CAAC,aAAa,GAAG,IAAI,wBAAwB,CAAC,IAAI,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAA;YACnF,CAAC;YACD,6FAA6F;YAC7F,MAAM,UAAU,GAAG,IAAI,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAA;YAE/C,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC5B,KAAK,CAAC,eAAe,GAAG,IAAI,0BAA0B,CAAC,UAAU,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAA;YAC7F,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;YAC5B,KAAK,CAAC,eAAe,EAAE,KAAK,EAAE,CAAA;QAC/B,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAA;IACpB,CAAC;CACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mailwoman/ban",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.9.0",
|
|
4
4
|
"description": "Base Adresse Nationale (France) rooftop address-point ingestion — builds the national FR precision shard on the situs address-point schema.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"address-point",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"test": "vitest run"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@mailwoman/core": "5.
|
|
50
|
-
"@mailwoman/resolver-wof-sqlite": "5.
|
|
49
|
+
"@mailwoman/core": "5.9.0",
|
|
50
|
+
"@mailwoman/resolver-wof-sqlite": "5.9.0",
|
|
51
51
|
"kysely": "^0.29.2",
|
|
52
52
|
"spliterator": "^3.2.0"
|
|
53
53
|
},
|