@mailwoman/corpus 5.9.0 → 5.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/out/src/adapter.d.ts +1 -1
  2. package/out/src/adapters/ban/adapter.d.ts +1 -1
  3. package/out/src/adapters/fcc-bdc/adapter.d.ts +1 -1
  4. package/out/src/adapters/geonames/adapter.d.ts +1 -1
  5. package/out/src/adapters/geonames-postal/adapter.d.ts +1 -1
  6. package/out/src/adapters/gnaf/adapter.d.ts +1 -1
  7. package/out/src/adapters/index.d.ts +19 -19
  8. package/out/src/adapters/openaddresses/adapter.d.ts +1 -1
  9. package/out/src/adapters/overture/adapter.d.ts +1 -1
  10. package/out/src/adapters/state-hi-schools/adapter.d.ts +1 -1
  11. package/out/src/adapters/state-ia-contractors/adapter.d.ts +1 -1
  12. package/out/src/adapters/state-ny-notaries/adapter.d.ts +1 -1
  13. package/out/src/adapters/state-tx-notaries/adapter.d.ts +1 -1
  14. package/out/src/adapters/synth-po-box/adapter.d.ts +2 -2
  15. package/out/src/adapters/tiger/adapter.d.ts +1 -1
  16. package/out/src/adapters/usgov-hrsa-fqhc/adapter.d.ts +1 -1
  17. package/out/src/adapters/usgov-imls-pls/adapter.d.ts +1 -1
  18. package/out/src/adapters/usgov-irs-bmf/adapter.d.ts +1 -1
  19. package/out/src/adapters/usgov-nad/adapter.d.ts +2 -2
  20. package/out/src/adapters/usgov-nad/adapter.js +1 -1
  21. package/out/src/adapters/usgov-nppes/adapter.d.ts +1 -1
  22. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.d.ts +1 -1
  23. package/out/src/adapters/wof-admin-jp/adapter.d.ts +1 -1
  24. package/out/src/adapters/wof-admin-json/adapter.d.ts +2 -2
  25. package/out/src/adapters/wof-postalcode-json/adapter.d.ts +2 -2
  26. package/out/src/align.d.ts +2 -2
  27. package/out/src/build.d.ts +4 -4
  28. package/out/src/index.d.ts +22 -21
  29. package/out/src/index.d.ts.map +1 -1
  30. package/out/src/index.js +1 -0
  31. package/out/src/index.js.map +1 -1
  32. package/out/src/parquet-wrapper/index.d.ts +3 -3
  33. package/out/src/parquet-wrapper/reader.d.ts +1 -1
  34. package/out/src/parquet-wrapper/writer.d.ts +4 -2
  35. package/out/src/parquet-wrapper/writer.d.ts.map +1 -1
  36. package/out/src/parquet-wrapper/writer.js +3 -1
  37. package/out/src/parquet-wrapper/writer.js.map +1 -1
  38. package/out/src/parquet.d.ts +3 -3
  39. package/out/src/runner.d.ts +2 -2
  40. package/out/src/shard-recipes/anchor-absorption.d.ts +1 -1
  41. package/out/src/shard-recipes/boundary-stress.d.ts +1 -1
  42. package/out/src/shard-recipes/country-balanced.d.ts +1 -1
  43. package/out/src/shard-recipes/cz-pcfirst-preposition.d.ts +1 -1
  44. package/out/src/shard-recipes/fr-admin-split.d.ts +1 -1
  45. package/out/src/shard-recipes/fr-bare-street.d.ts +1 -1
  46. package/out/src/shard-recipes/fr-order.d.ts +1 -1
  47. package/out/src/shard-recipes/german.d.ts +1 -1
  48. package/out/src/shard-recipes/house-venue.d.ts +1 -1
  49. package/out/src/shard-recipes/index.d.ts +2 -2
  50. package/out/src/shard-recipes/intersection.d.ts +1 -1
  51. package/out/src/shard-recipes/locale.d.ts +2 -2
  52. package/out/src/shard-recipes/nl-postcode.d.ts +1 -1
  53. package/out/src/shard-recipes/no-street-led.d.ts +1 -1
  54. package/out/src/shard-recipes/no-street.d.ts +1 -1
  55. package/out/src/shard-recipes/po-box-cedex.d.ts +1 -1
  56. package/out/src/shard-recipes/po-box.d.ts +1 -1
  57. package/out/src/shard-recipes/si-bare-village.d.ts +1 -1
  58. package/out/src/shard-recipes/street-affix.d.ts +1 -1
  59. package/out/src/shard-recipes/street-bare.d.ts +1 -1
  60. package/out/src/shard-recipes/street.d.ts +1 -1
  61. package/out/src/shard-recipes/unit.d.ts +1 -1
  62. package/out/src/split.d.ts +1 -1
  63. package/out/src/split.js +1 -1
  64. package/out/src/split.js.map +1 -1
  65. package/out/src/synthesize-boundary-stress.d.ts +1 -1
  66. package/out/src/synthesize-german.d.ts +1 -1
  67. package/out/src/synthesize-house-venue.d.ts +1 -1
  68. package/out/src/synthesize-intersection.d.ts +1 -1
  69. package/out/src/synthesize-no-street.d.ts +1 -1
  70. package/out/src/synthesize-po-box.d.ts +1 -1
  71. package/out/src/synthesize-street.d.ts +1 -1
  72. package/out/src/synthesize.d.ts +2 -2
  73. package/out/src/tools/align-shard.d.ts +8 -0
  74. package/out/src/tools/align-shard.d.ts.map +1 -0
  75. package/out/src/tools/align-shard.js +58 -0
  76. package/out/src/tools/align-shard.js.map +1 -0
  77. package/out/src/tools/audit.d.ts +28 -0
  78. package/out/src/tools/audit.d.ts.map +1 -0
  79. package/out/src/tools/audit.js +273 -0
  80. package/out/src/tools/audit.js.map +1 -0
  81. package/out/src/tools/corpus-stats.d.ts +44 -0
  82. package/out/src/tools/corpus-stats.d.ts.map +1 -0
  83. package/out/src/tools/corpus-stats.js +151 -0
  84. package/out/src/tools/corpus-stats.js.map +1 -0
  85. package/out/src/tools/fetch/ban.d.ts +31 -0
  86. package/out/src/tools/fetch/ban.d.ts.map +1 -0
  87. package/out/src/tools/fetch/ban.js +248 -0
  88. package/out/src/tools/fetch/ban.js.map +1 -0
  89. package/out/src/tools/fetch/download.d.ts +49 -0
  90. package/out/src/tools/fetch/download.d.ts.map +1 -0
  91. package/out/src/tools/fetch/download.js +81 -0
  92. package/out/src/tools/fetch/download.js.map +1 -0
  93. package/out/src/tools/fetch/hrsa.d.ts +18 -0
  94. package/out/src/tools/fetch/hrsa.d.ts.map +1 -0
  95. package/out/src/tools/fetch/hrsa.js +46 -0
  96. package/out/src/tools/fetch/hrsa.js.map +1 -0
  97. package/out/src/tools/fetch/imls-pls.d.ts +25 -0
  98. package/out/src/tools/fetch/imls-pls.d.ts.map +1 -0
  99. package/out/src/tools/fetch/imls-pls.js +120 -0
  100. package/out/src/tools/fetch/imls-pls.js.map +1 -0
  101. package/out/src/tools/fetch/index.d.ts +118 -0
  102. package/out/src/tools/fetch/index.d.ts.map +1 -0
  103. package/out/src/tools/fetch/index.js +117 -0
  104. package/out/src/tools/fetch/index.js.map +1 -0
  105. package/out/src/tools/fetch/nad.d.ts +56 -0
  106. package/out/src/tools/fetch/nad.d.ts.map +1 -0
  107. package/out/src/tools/fetch/nad.js +225 -0
  108. package/out/src/tools/fetch/nad.js.map +1 -0
  109. package/out/src/tools/fetch/nppes.d.ts +26 -0
  110. package/out/src/tools/fetch/nppes.d.ts.map +1 -0
  111. package/out/src/tools/fetch/nppes.js +136 -0
  112. package/out/src/tools/fetch/nppes.js.map +1 -0
  113. package/out/src/tools/fetch/openaddresses.d.ts +59 -0
  114. package/out/src/tools/fetch/openaddresses.d.ts.map +1 -0
  115. package/out/src/tools/fetch/openaddresses.js +307 -0
  116. package/out/src/tools/fetch/openaddresses.js.map +1 -0
  117. package/out/src/tools/fetch/state-hi-schools.d.ts +25 -0
  118. package/out/src/tools/fetch/state-hi-schools.d.ts.map +1 -0
  119. package/out/src/tools/fetch/state-hi-schools.js +171 -0
  120. package/out/src/tools/fetch/state-hi-schools.js.map +1 -0
  121. package/out/src/tools/fetch/state-sources.d.ts +22 -0
  122. package/out/src/tools/fetch/state-sources.d.ts.map +1 -0
  123. package/out/src/tools/fetch/state-sources.js +110 -0
  124. package/out/src/tools/fetch/state-sources.js.map +1 -0
  125. package/out/src/tools/fetch/tiger-full.d.ts +36 -0
  126. package/out/src/tools/fetch/tiger-full.d.ts.map +1 -0
  127. package/out/src/tools/fetch/tiger-full.js +239 -0
  128. package/out/src/tools/fetch/tiger-full.js.map +1 -0
  129. package/out/src/tools/golden-expand.d.ts +74 -0
  130. package/out/src/tools/golden-expand.d.ts.map +1 -0
  131. package/out/src/tools/golden-expand.js +394 -0
  132. package/out/src/tools/golden-expand.js.map +1 -0
  133. package/out/src/tools/golden-promote.d.ts +60 -0
  134. package/out/src/tools/golden-promote.d.ts.map +1 -0
  135. package/out/src/tools/golden-promote.js +223 -0
  136. package/out/src/tools/golden-promote.js.map +1 -0
  137. package/out/src/tools/index.d.ts +24 -0
  138. package/out/src/tools/index.d.ts.map +1 -0
  139. package/out/src/tools/index.js +24 -0
  140. package/out/src/tools/index.js.map +1 -0
  141. package/out/src/tools/ingest-csv.d.ts +45 -0
  142. package/out/src/tools/ingest-csv.d.ts.map +1 -0
  143. package/out/src/tools/ingest-csv.js +309 -0
  144. package/out/src/tools/ingest-csv.js.map +1 -0
  145. package/out/src/tools/jsonl-to-parquet.d.ts +49 -0
  146. package/out/src/tools/jsonl-to-parquet.d.ts.map +1 -0
  147. package/out/src/tools/jsonl-to-parquet.js +157 -0
  148. package/out/src/tools/jsonl-to-parquet.js.map +1 -0
  149. package/out/src/tools/lint-shard-vocab.d.ts +66 -0
  150. package/out/src/tools/lint-shard-vocab.d.ts.map +1 -0
  151. package/out/src/tools/lint-shard-vocab.js +266 -0
  152. package/out/src/tools/lint-shard-vocab.js.map +1 -0
  153. package/out/src/tools/lint-shard.d.ts +73 -0
  154. package/out/src/tools/lint-shard.d.ts.map +1 -0
  155. package/out/src/tools/lint-shard.js +350 -0
  156. package/out/src/tools/lint-shard.js.map +1 -0
  157. package/out/src/tools/overlay-manifest.d.ts +35 -0
  158. package/out/src/tools/overlay-manifest.d.ts.map +1 -0
  159. package/out/src/tools/overlay-manifest.js +85 -0
  160. package/out/src/tools/overlay-manifest.js.map +1 -0
  161. package/out/src/tools/shard-kryptonite.d.ts +28 -0
  162. package/out/src/tools/shard-kryptonite.d.ts.map +1 -0
  163. package/out/src/tools/shard-kryptonite.js +101 -0
  164. package/out/src/tools/shard-kryptonite.js.map +1 -0
  165. package/out/src/tools/shard-translit.d.ts +42 -0
  166. package/out/src/tools/shard-translit.d.ts.map +1 -0
  167. package/out/src/tools/shard-translit.js +196 -0
  168. package/out/src/tools/shard-translit.js.map +1 -0
  169. package/package.json +63 -7
@@ -0,0 +1,307 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Fetch an OpenAddresses country collection from batch.openaddresses.io.
7
+ *
8
+ * Source: https://batch.openaddresses.io
9
+ * License: MIXED — OpenAddresses aggregates hundreds of upstream sources with per-source licenses
10
+ * (CC-BY, CC0, PDDL, ODbL, CC-BY-SA, and proprietary attribution-only). The per-row LICENSE filter
11
+ * in the openaddresses adapter is essential for proprietary-weights training: Tier-C rows (ODbL,
12
+ * CC-BY-SA, CC-SA) are dropped at ingest by default. This module downloads the raw collection; the
13
+ * adapter does the license gating.
14
+ *
15
+ * Native `fetch` streams the download to disk (no curl + Python subprocess tax);
16
+ * `node:child_process` keeps the genuine shell ops it still needs (`file` magic detection +
17
+ * `gunzip` decompression, both nice/ionice-deprioritized).
18
+ *
19
+ * ## Authentication note (2026-05-18)
20
+ *
21
+ * The batch.openaddresses.io download endpoint now requires a registered account. Downloads are
22
+ * still free at the "basic" tier (GeoJSON+LD output).
23
+ *
24
+ * 1. Register at https://batch.openaddresses.io/register
25
+ * 2. Log in and go to Profile → "Create Token"
26
+ * 3. Export the token: `export OA_BATCH_TOKEN=<your-token>`
27
+ * 4. Re-run the command.
28
+ *
29
+ * The collection URL pattern (verified 2026-05-18):
30
+ *
31
+ * - `POST /api/login {username, password}` → `{token}`
32
+ * - `GET /api/job/{job_id}/output/source.geojson.gz?token={token}`
33
+ *
34
+ * Collections are downloaded as a combined GeoJSON.gz via:
35
+ *
36
+ * - `GET /api/collections/{collection_id}/download` (returns a redirect to S3)
37
+ *
38
+ * Collection IDs discovered from `/api/collections`:
39
+ *
40
+ * - `id=6 name="ca" size=2044467556` (~1.9 GiB uncompressed, verified 2026-05-18)
41
+ *
42
+ * ## Usage
43
+ *
44
+ * ```sh
45
+ * # With token (preferred). Default country: ca. Supports any OA country code (us-west, fr, …)
46
+ * OA_BATCH_TOKEN=<token> mailwoman corpus fetch openaddresses --country ca \
47
+ * --out-root /mnt/playpen/mailwoman-data/corpus/sources
48
+ *
49
+ * # Without token (will detect + print instructions, then report the failure):
50
+ * mailwoman corpus fetch openaddresses --country ca
51
+ * ```
52
+ */
53
+ import { execFile, spawn } from "node:child_process";
54
+ import { createReadStream, createWriteStream, existsSync, mkdirSync, renameSync, rmSync, statSync } from "node:fs";
55
+ import { join } from "node:path";
56
+ import { Readable } from "node:stream";
57
+ import { pipeline } from "node:stream/promises";
58
+ import { setTimeout as sleep } from "node:timers/promises";
59
+ import { promisify } from "node:util";
60
+ import { $private } from "@mailwoman/core/env";
61
+ import { sha256File } from "@mailwoman/core/utils";
62
+ import { isTransientStatus, writeManifest } from "./download.js";
63
+ const execFileAsync = promisify(execFile);
64
+ const OA_BASE = "https://batch.openaddresses.io";
65
+ /**
66
+ * Collection IDs known as of 2026-05-18 (discovered via `GET /api/collections`). OA assigns stable integer IDs to each
67
+ * country collection; re-check `GET /api/collections` if a new country is needed and the ID is unknown.
68
+ */
69
+ const OA_COLLECTION_IDS = {
70
+ ca: 6,
71
+ "us-west": 4,
72
+ "us-south": 3,
73
+ "us-northeast": 2,
74
+ "us-midwest": 5,
75
+ global: 1,
76
+ };
77
+ /** Stream-count newlines, matching `wc -l` (memory-safe for the multi-GB collection). */
78
+ async function countLines(path) {
79
+ let count = 0;
80
+ for await (const chunk of createReadStream(path)) {
81
+ for (let i = 0; i < chunk.length; i++) {
82
+ if (chunk[i] === 0x0a) {
83
+ count++;
84
+ }
85
+ }
86
+ }
87
+ return count;
88
+ }
89
+ function humanBytes(bytes) {
90
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
91
+ let value = bytes;
92
+ let unit = 0;
93
+ while (value >= 1024 && unit < units.length - 1) {
94
+ value /= 1024;
95
+ unit++;
96
+ }
97
+ return `${value.toFixed(unit === 0 ? 0 : 1)}${units[unit]}`;
98
+ }
99
+ /**
100
+ * Stream an HTTP download to disk, returning the final HTTP status (0 on network error after retries). Follows
101
+ * redirects (the OA download endpoint 302s to a pre-signed S3 URL).
102
+ *
103
+ * NOTE(phase1): kept local instead of the shared `downloadToFile` — this one streams a multi-GB body to disk (the
104
+ * shared util buffers via `arrayBuffer()`) and returns the HTTP status instead of throwing, which the caller needs for
105
+ * its two-URL fallback ladder.
106
+ */
107
+ async function streamDownload(url, dest, opts) {
108
+ for (let attempt = 0; attempt <= opts.retries; attempt++) {
109
+ try {
110
+ const res = await fetch(url, {
111
+ headers: opts.headers ?? {},
112
+ redirect: "follow",
113
+ signal: AbortSignal.timeout(opts.timeoutMs),
114
+ });
115
+ if (res.ok && res.body) {
116
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
117
+ return res.status;
118
+ }
119
+ if (attempt < opts.retries && isTransientStatus(res.status)) {
120
+ await sleep(opts.retryDelayMs);
121
+ continue;
122
+ }
123
+ return res.status;
124
+ }
125
+ catch {
126
+ if (attempt < opts.retries) {
127
+ await sleep(opts.retryDelayMs);
128
+ continue;
129
+ }
130
+ return 0;
131
+ }
132
+ }
133
+ return 0;
134
+ }
135
+ /** Decompress `src` → `dest` with the same deprioritized subprocess the old fetcher used. */
136
+ async function gunzipToFile(src, dest) {
137
+ const child = spawn("nice", ["-n", "15", "ionice", "-c", "3", "gunzip", "-c", src], {
138
+ stdio: ["ignore", "pipe", "inherit"],
139
+ });
140
+ await pipeline(child.stdout, createWriteStream(dest));
141
+ await new Promise((resolve, reject) => {
142
+ child.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`gunzip exited with code ${code}`))));
143
+ child.on("error", reject);
144
+ });
145
+ }
146
+ export async function fetchOpenAddresses(options, report) {
147
+ const country = options.country ?? "ca";
148
+ const token = $private.OA_BATCH_TOKEN;
149
+ const destDir = join(options.outRoot, "openaddresses", country);
150
+ const manifestPath = join(destDir, "MANIFEST.json");
151
+ const outputFile = join(destDir, "collection.geojsonl");
152
+ const fail = (code) => ({ fetched: 0, skipped: 0, failed: 1, failedCodes: [code] });
153
+ report?.(`=== fetch openaddresses: country=${country}`);
154
+ report?.(` dest: ${destDir}`);
155
+ mkdirSync(destDir, { recursive: true });
156
+ // -------------------------------------------------------------------------
157
+ // Authentication check
158
+ // -------------------------------------------------------------------------
159
+ if (!token) {
160
+ report?.(`
161
+ ERROR: OA_BATCH_TOKEN is not set.
162
+
163
+ As of 2026-05-18, batch.openaddresses.io requires a registered (free) account
164
+ to download collection files. Data remains openly licensed — the auth gate
165
+ is there to prevent CDN abuse, not to restrict access.
166
+
167
+ Steps to get a token:
168
+ 1. Register at: https://batch.openaddresses.io/register
169
+ 2. Verify your email and log in.
170
+ 3. Go to Profile → "Create Token" → copy the token.
171
+ 4. Export it in this shell:
172
+ export OA_BATCH_TOKEN=<your-token>
173
+ 5. Re-run this command.
174
+
175
+ The Canada collection (ca) is ~2 GiB compressed / ~7 GiB uncompressed
176
+ (estimated), so budget ~20–45 minutes at typical cloud-to-host bandwidth.
177
+ `);
178
+ return fail("OA_BATCH_TOKEN");
179
+ }
180
+ // -------------------------------------------------------------------------
181
+ // Determine collection ID
182
+ // -------------------------------------------------------------------------
183
+ let collectionID = OA_COLLECTION_IDS[country];
184
+ if (collectionID === undefined) {
185
+ report?.(`Unknown country code '${country}'. Fetching collection list to find ID...`);
186
+ const res = await fetch(`${OA_BASE}/api/collections`, {
187
+ headers: { Authorization: `Bearer ${token}`, "Accept-Encoding": "gzip, br" },
188
+ signal: AbortSignal.timeout(30_000),
189
+ });
190
+ if (!res.ok) {
191
+ report?.(`ERROR: GET /api/collections returned HTTP ${res.status}.`);
192
+ return fail(country);
193
+ }
194
+ const collections = (await res.json());
195
+ const match = collections.find((item) => item.name === country);
196
+ if (match?.id === undefined) {
197
+ report?.(`ERROR: Could not find a collection named '${country}' in GET /api/collections.`);
198
+ report?.(`Available collections:`);
199
+ for (const item of collections) {
200
+ const size = (item.size ?? 0).toLocaleString();
201
+ report?.(` ${(item.name ?? "").padEnd(20)} id=${item.id} ${item.human ?? ""} size=${size} bytes`);
202
+ }
203
+ return fail(country);
204
+ }
205
+ collectionID = match.id;
206
+ report?.(` Found collection id=${collectionID} for '${country}'`);
207
+ }
208
+ // -------------------------------------------------------------------------
209
+ // Download via the collections download endpoint (302s to S3)
210
+ // -------------------------------------------------------------------------
211
+ report?.(` Resolving download URL for collection id=${collectionID}...`);
212
+ report?.(` Attempting authenticated download...`);
213
+ const tmpGz = join(destDir, "collection.geojsonl.gz.tmp");
214
+ const tmpRaw = join(destDir, "collection.geojsonl.tmp");
215
+ const sourceURL = `${OA_BASE}/api/collections/${collectionID}/download`;
216
+ let httpStatus = await streamDownload(sourceURL, tmpGz, {
217
+ headers: { Authorization: `Bearer ${token}` },
218
+ timeoutMs: 7_200_000,
219
+ retries: 3,
220
+ retryDelayMs: 30_000,
221
+ });
222
+ if (httpStatus !== 200) {
223
+ // Try the geojsonl.gz directly with token as query param (alternate URL shape).
224
+ httpStatus = await streamDownload(`${OA_BASE}/api/collections/${collectionID}/geojsonl.gz?token=${token}`, tmpGz, {
225
+ timeoutMs: 7_200_000,
226
+ retries: 3,
227
+ retryDelayMs: 30_000,
228
+ });
229
+ }
230
+ if (httpStatus !== 200) {
231
+ rmSync(tmpGz, { force: true });
232
+ report?.(`
233
+ ERROR: Download returned HTTP ${httpStatus}.
234
+
235
+ Likely causes:
236
+ 1. OA_BATCH_TOKEN is invalid or expired — re-create it at Profile → Tokens.
237
+ 2. The collection download endpoint URL has changed (this module was written
238
+ against the 2026-05-18 batch.openaddresses.io API; it may need updating).
239
+ 3. Network error or CDN outage.
240
+
241
+ Manual download (after logging in to batch.openaddresses.io):
242
+ - Navigate to https://batch.openaddresses.io/collection/${collectionID}
243
+ - Click "GeoJSON+LD" to download the collection.
244
+ - Save as: ${outputFile}
245
+
246
+ URL tried: ${OA_BASE}/api/collections/${collectionID}/download
247
+ `);
248
+ return fail(country);
249
+ }
250
+ // -------------------------------------------------------------------------
251
+ // Decompress if the downloaded file is gzipped
252
+ // -------------------------------------------------------------------------
253
+ const fileMagic = (await execFileAsync("file", ["--brief", tmpGz]).catch(() => ({ stdout: "" }))).stdout;
254
+ if (/gzip|compressed/i.test(fileMagic)) {
255
+ report?.(` Decompressing gzip archive...`);
256
+ await gunzipToFile(tmpGz, tmpRaw);
257
+ rmSync(tmpGz, { force: true });
258
+ renameSync(tmpRaw, outputFile);
259
+ }
260
+ else if (/JSON|ASCII|UTF-8/i.test(fileMagic)) {
261
+ // Already line-delimited GeoJSON.
262
+ renameSync(tmpGz, outputFile);
263
+ rmSync(tmpRaw, { force: true });
264
+ }
265
+ else {
266
+ // Unknown type — keep as-is and let the operator inspect.
267
+ renameSync(tmpGz, outputFile);
268
+ report?.(` WARNING: Downloaded file type is '${fileMagic.trim()}' — may need manual decompression.`);
269
+ }
270
+ // -------------------------------------------------------------------------
271
+ // Verify + write MANIFEST
272
+ // -------------------------------------------------------------------------
273
+ if (!existsSync(outputFile)) {
274
+ report?.(`ERROR: Output file not found at ${outputFile} after download.`);
275
+ return fail(country);
276
+ }
277
+ const size = statSync(outputFile).size;
278
+ if (size < 10240) {
279
+ report?.(`ERROR: File is suspiciously small (${size} bytes) — likely an error response.`);
280
+ return fail(country);
281
+ }
282
+ const sha = await sha256File(outputFile);
283
+ const rowCount = await countLines(outputFile);
284
+ const downloadedAt = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
285
+ const manifest = {
286
+ source_url: sourceURL,
287
+ collection_id: collectionID,
288
+ country,
289
+ filename: "collection.geojsonl",
290
+ downloaded_at: downloadedAt,
291
+ sha256: sha,
292
+ bytes: size,
293
+ row_count: rowCount,
294
+ notes: "batch.openaddresses.io requires a free registered account for downloads. License is mixed per-row; use the openaddresses adapter with allowShareAlike=false (default) to filter Tier-C rows.",
295
+ };
296
+ await writeManifest(manifestPath, manifest);
297
+ report?.(` ✓ ${humanBytes(size)} rows=${rowCount} sha256=${sha}`);
298
+ report?.(` MANIFEST written to ${manifestPath}`);
299
+ report?.(`=== done`);
300
+ report?.(`Feed to the adapter:`);
301
+ report?.(` mailwoman corpus run openaddresses \\`);
302
+ report?.(` --input ${outputFile} \\`);
303
+ report?.(` --country ${country.toUpperCase()} \\`);
304
+ report?.(` --output ${options.outRoot}`);
305
+ return { fetched: 1, skipped: 0, failed: 0, failedCodes: [] };
306
+ }
307
+ //# sourceMappingURL=openaddresses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openaddresses.js","sourceRoot":"","sources":["../../../../src/tools/fetch/openaddresses.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
+ * Re-fetch the Hawaii State DOE school directory and convert the XLSX workbook to a flat CSV the
7
+ * `state-hi-schools` adapter can consume.
8
+ *
9
+ * Upstream is a single XLSX (~64 KB) with two sheets — `HIDOE` (~258 district schools) and `PCS`
10
+ * (~38 public charter schools). Both sheets share the same header. This module concatenates them
11
+ * under one shared header so the adapter can stream a single CSV.
12
+ *
13
+ * License: Hawaii state government open data (Tier A — state PD-equivalent).
14
+ *
15
+ * Built-in `fetch` (gzip/brotli) replaces curl for the download; the XLSX → CSV step still rides
16
+ * `python3` + `openpyxl` via `node:child_process` — there is no clean node equivalent without
17
+ * adding a workbook-parsing dependency.
18
+ *
19
+ * Invoke via `mailwoman corpus fetch state-hi-schools --out-root <path>`. Idempotent: if the dest
20
+ * CSV exists and sha matches MANIFEST, skips download.
21
+ */
22
+ import type { BaseFetchOptions, FetchSummary } from "./download.ts";
23
+ export type FetchStateHISchoolsOptions = BaseFetchOptions;
24
+ export declare function fetchStateHISchools(options: FetchStateHISchoolsOptions, report?: (line: string) => void): Promise<FetchSummary>;
25
+ //# sourceMappingURL=state-hi-schools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-hi-schools.d.ts","sourceRoot":"","sources":["../../../../src/tools/fetch/state-hi-schools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AASH,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAQnE,MAAM,MAAM,0BAA0B,GAAG,gBAAgB,CAAA;AAsFzD,wBAAsB,mBAAmB,CACxC,OAAO,EAAE,0BAA0B,EACnC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC7B,OAAO,CAAC,YAAY,CAAC,CA0FvB"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Re-fetch the Hawaii State DOE school directory and convert the XLSX workbook to a flat CSV the
7
+ * `state-hi-schools` adapter can consume.
8
+ *
9
+ * Upstream is a single XLSX (~64 KB) with two sheets — `HIDOE` (~258 district schools) and `PCS`
10
+ * (~38 public charter schools). Both sheets share the same header. This module concatenates them
11
+ * under one shared header so the adapter can stream a single CSV.
12
+ *
13
+ * License: Hawaii state government open data (Tier A — state PD-equivalent).
14
+ *
15
+ * Built-in `fetch` (gzip/brotli) replaces curl for the download; the XLSX → CSV step still rides
16
+ * `python3` + `openpyxl` via `node:child_process` — there is no clean node equivalent without
17
+ * adding a workbook-parsing dependency.
18
+ *
19
+ * Invoke via `mailwoman corpus fetch state-hi-schools --out-root <path>`. Idempotent: if the dest
20
+ * CSV exists and sha matches MANIFEST, skips download.
21
+ */
22
+ import { spawn, spawnSync } from "node:child_process";
23
+ import { existsSync, mkdirSync, statSync } from "node:fs";
24
+ import { unlink } from "node:fs/promises";
25
+ import { join } from "node:path";
26
+ import { sha256File } from "@mailwoman/core/utils";
27
+ import { downloadToFile, readManifest, writeManifest } from "./download.js";
28
+ const SOURCE_URL = "https://www.hawaiipublicschools.org/DOE%20Forms/SchoolList.xlsx";
29
+ const SLUG = "state-hi-schools";
30
+ const CSV_FILENAME = "HI_Public_Schools_List.csv";
31
+ const XLSX_FILENAME = "HI_Public_Schools_List.xlsx";
32
+ /**
33
+ * The XLSX → CSV converter: concatenate every sheet under one shared header (the first sheet's). Runs as `python3 -c
34
+ * <script> <xlsx-path> <csv-path>`, so `sys.argv[1]`/`sys.argv[2]` are the I/O paths. TODO: Get rid of this.
35
+ */
36
+ const PY_CONVERT = `
37
+ import csv
38
+ import sys
39
+ from openpyxl import load_workbook
40
+
41
+ xlsx_path, csv_path = sys.argv[1], sys.argv[2]
42
+ wb = load_workbook(xlsx_path, data_only=True, read_only=True)
43
+
44
+ with open(csv_path, "w", newline="", encoding="utf-8") as out:
45
+ writer = csv.writer(out)
46
+ shared_header = None
47
+ total_data_rows = 0
48
+ for sheet_name in wb.sheetnames:
49
+ ws = wb[sheet_name]
50
+ rows = ws.iter_rows(values_only=True)
51
+ try:
52
+ header = next(rows)
53
+ except StopIteration:
54
+ continue
55
+ norm_header = ["" if v is None else str(v).strip() for v in header]
56
+ if shared_header is None:
57
+ shared_header = norm_header
58
+ writer.writerow(shared_header)
59
+ elif norm_header != shared_header:
60
+ print(
61
+ f" ! sheet '{sheet_name}' header diverges from shared header; concatenating anyway",
62
+ file=sys.stderr,
63
+ )
64
+ for row in rows:
65
+ if row is None:
66
+ continue
67
+ # Skip fully-empty rows (XLSX iter_rows can yield phantom trailing rows).
68
+ if all(v is None or (isinstance(v, str) and not v.strip()) for v in row):
69
+ continue
70
+ writer.writerow(["" if v is None else str(v).strip() for v in row])
71
+ total_data_rows += 1
72
+
73
+ print(f" converted {total_data_rows} data rows from {len(wb.sheetnames)} sheets", file=sys.stderr)
74
+ `;
75
+ /** Mimic `numfmt --to=iec` for a friendly byte-size log line. */
76
+ function iec(bytes) {
77
+ if (bytes < 1024)
78
+ return String(bytes);
79
+ const units = ["K", "M", "G", "T", "P"];
80
+ let value = bytes / 1024;
81
+ let i = 0;
82
+ while (value >= 1024 && i < units.length - 1) {
83
+ value /= 1024;
84
+ i++;
85
+ }
86
+ const rounded = value < 10 ? value.toFixed(1) : Math.round(value).toString();
87
+ return `${rounded}${units[i] ?? ""}`;
88
+ }
89
+ /**
90
+ * Run the openpyxl converter. Its stderr narration streams straight through to the process stderr (matching the old
91
+ * `stdio: inherit` behavior) rather than routing through `report` — the python child owns those lines.
92
+ */
93
+ async function convertXLSXToCSV(xlsxPath, csvPath) {
94
+ const child = spawn("python3", ["-c", PY_CONVERT, xlsxPath, csvPath], {
95
+ stdio: ["ignore", "inherit", "inherit"],
96
+ });
97
+ await new Promise((resolve, reject) => {
98
+ child.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`python3 converter exited with ${code}`))));
99
+ child.on("error", reject);
100
+ });
101
+ }
102
+ export async function fetchStateHISchools(options, report) {
103
+ const destDir = join(options.outRoot, SLUG);
104
+ mkdirSync(destDir, { recursive: true });
105
+ const xlsxDest = join(destDir, XLSX_FILENAME);
106
+ const csvDest = join(destDir, CSV_FILENAME);
107
+ const manifestPath = join(destDir, "MANIFEST.json");
108
+ report?.(`=== ${SLUG}`);
109
+ // Idempotency: skip if CSV exists and sha matches recorded MANIFEST.
110
+ if (existsSync(csvDest)) {
111
+ const recorded = await readManifest(manifestPath);
112
+ if (recorded?.sha256 && recorded.filename === CSV_FILENAME) {
113
+ const actualSha = await sha256File(csvDest);
114
+ if (actualSha === recorded.sha256) {
115
+ report?.(` ✓ Already current (sha256 matches MANIFEST) — skipping download.`);
116
+ return { fetched: 0, skipped: 1, failed: 0, failedCodes: [] };
117
+ }
118
+ }
119
+ }
120
+ // Preflight: openpyxl must be importable.
121
+ const preflight = spawnSync("python3", ["-c", "import openpyxl"], { stdio: "ignore" });
122
+ if (preflight.status !== 0) {
123
+ report?.(` ✗ python3 with the \`openpyxl\` package is required to convert the HIDOE XLSX.\n` +
124
+ ` Debian/Ubuntu: sudo apt-get install -y python3-openpyxl\n` +
125
+ ` macOS Homebrew: brew install python && pip3 install openpyxl`);
126
+ return { fetched: 0, skipped: 0, failed: 1, failedCodes: [SLUG] };
127
+ }
128
+ // Download XLSX.
129
+ report?.(` Downloading ${SOURCE_URL} ...`);
130
+ try {
131
+ await downloadToFile({
132
+ url: SOURCE_URL,
133
+ dest: xlsxDest,
134
+ timeoutMs: 600_000,
135
+ headers: { "Accept-Encoding": "gzip, br" },
136
+ report,
137
+ });
138
+ }
139
+ catch (err) {
140
+ report?.(` ✗ Download failed (${err.message})`);
141
+ return { fetched: 0, skipped: 0, failed: 1, failedCodes: [SLUG] };
142
+ }
143
+ const xlsxSize = statSync(xlsxDest).size;
144
+ report?.(` Downloaded XLSX: ${iec(xlsxSize)}`);
145
+ if (xlsxSize < 1024) {
146
+ report?.(` ✗ Response too small (${xlsxSize} bytes) — probable error page`);
147
+ return { fetched: 0, skipped: 0, failed: 1, failedCodes: [SLUG] };
148
+ }
149
+ // Convert XLSX → CSV (concatenate both sheets under one shared header).
150
+ report?.(` Converting XLSX → CSV (concatenating sheets) ...`);
151
+ await convertXLSXToCSV(xlsxDest, csvDest);
152
+ const csvSize = statSync(csvDest).size;
153
+ const csvSha = await sha256File(csvDest);
154
+ // Remove XLSX (CSV is the canonical artifact the adapter consumes).
155
+ await unlink(xlsxDest);
156
+ report?.(` Removed XLSX (CSV kept)`);
157
+ // Write MANIFEST.
158
+ const manifest = {
159
+ source_url: SOURCE_URL,
160
+ downloaded_at: new Date().toISOString(),
161
+ filename: CSV_FILENAME,
162
+ sha256: csvSha,
163
+ bytes: csvSize,
164
+ notes: "Converted from XLSX (sheets HIDOE + PCS concatenated under shared header).",
165
+ };
166
+ await writeManifest(manifestPath, manifest);
167
+ report?.(` ✓ ${iec(csvSize)} sha256=${csvSha}`);
168
+ report?.(` MANIFEST written to ${manifestPath}`);
169
+ return { fetched: 1, skipped: 0, failed: 0, failedCodes: [] };
170
+ }
171
+ //# sourceMappingURL=state-hi-schools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-hi-schools.js","sourceRoot":"","sources":["../../../../src/tools/fetch/state-hi-schools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAGlD,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAE3E,MAAM,UAAU,GAAG,iEAAiE,CAAA;AACpF,MAAM,IAAI,GAAG,kBAAkB,CAAA;AAC/B,MAAM,YAAY,GAAG,4BAA4B,CAAA;AACjD,MAAM,aAAa,GAAG,6BAA6B,CAAA;AAInD;;;GAGG;AACH,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsClB,CAAA;AAWD,iEAAiE;AACjE,SAAS,GAAG,CAAC,KAAa;IACzB,IAAI,KAAK,GAAG,IAAI;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;IACtC,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;IACvC,IAAI,KAAK,GAAG,KAAK,GAAG,IAAI,CAAA;IACxB,IAAI,CAAC,GAAG,CAAC,CAAA;IAET,OAAO,KAAK,IAAI,IAAI,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9C,KAAK,IAAI,IAAI,CAAA;QACb,CAAC,EAAE,CAAA;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAA;IAE5E,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAA;AACrC,CAAC;AAED;;;GAGG;AACH,KAAK,UAAU,gBAAgB,CAAC,QAAgB,EAAE,OAAe;IAChE,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE;QACrE,KAAK,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC;KACvC,CAAC,CAAA;IACF,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3C,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,iCAAiC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;QAClH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IAC1B,CAAC,CAAC,CAAA;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACxC,OAAmC,EACnC,MAA+B;IAE/B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IAC3C,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAEvC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAA;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;IAC3C,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC,CAAA;IAEnD,MAAM,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC,CAAA;IAEvB,qEAAqE;IACrE,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAoB,YAAY,CAAC,CAAA;QAEpE,IAAI,QAAQ,EAAE,MAAM,IAAI,QAAQ,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;YAC5D,MAAM,SAAS,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,CAAA;YAE3C,IAAI,SAAS,KAAK,QAAQ,CAAC,MAAM,EAAE,CAAC;gBACnC,MAAM,EAAE,CAAC,oEAAoE,CAAC,CAAA;gBAE9E,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,CAAA;YAC9D,CAAC;QACF,CAAC;IACF,CAAC;IAED,0CAA0C;IAC1C,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,iBAAiB,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAA;IAEtF,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,EAAE,CACP,oFAAoF;YACnF,gEAAgE;YAChE,kEAAkE,CACnE,CAAA;QAED,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,IAAI,CAAC,EAAE,CAAA;IAClE,CAAC;IAED,iBAAiB;IACjB,MAAM,EAAE,CAAC,iBAAiB,UAAU,MAAM,CAAC,CAAA;IAE3C,IAAI,CAAC;QACJ,MAAM,cAAc,CAAC;YACpB,GAAG,EAAE,UAAU;YACf,IAAI,EAAE,QAAQ;YACd,SAAS,EAAE,OAAO;YAClB,OAAO,EAAE,EAAE,iBAAiB,EAAE,UAAU,EAAE;YAC1C,MAAM;SACN,CAAC,CAAA;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,EAAE,CAAC,wBAAyB,GAAa,CAAC,OAAO,GAAG,CAAC,CAAA;QAE3D,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,IAAI,CAAC,EAAE,CAAA;IAClE,CAAC;IAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAA;IACxC,MAAM,EAAE,CAAC,sBAAsB,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;IAE/C,IAAI,QAAQ,GAAG,IAAI,EAAE,CAAC;QACrB,MAAM,EAAE,CAAC,2BAA2B,QAAQ,+BAA+B,CAAC,CAAA;QAE5E,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,IAAI,CAAC,EAAE,CAAA;IAClE,CAAC;IAED,wEAAwE;IACxE,MAAM,EAAE,CAAC,oDAAoD,CAAC,CAAA;IAC9D,MAAM,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IAEzC,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAA;IACtC,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,CAAA;IAExC,oEAAoE;IACpE,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAA;IACtB,MAAM,EAAE,CAAC,2BAA2B,CAAC,CAAA;IAErC,kBAAkB;IAClB,MAAM,QAAQ,GAAa;QAC1B,UAAU,EAAE,UAAU;QACtB,aAAa,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACvC,QAAQ,EAAE,YAAY;QACtB,MAAM,EAAE,MAAM;QACd,KAAK,EAAE,OAAO;QACd,KAAK,EAAE,4EAA4E;KACnF,CAAA;IACD,MAAM,aAAa,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAA;IAE3C,MAAM,EAAE,CAAC,OAAO,GAAG,CAAC,OAAO,CAAC,YAAY,MAAM,EAAE,CAAC,CAAA;IACjD,MAAM,EAAE,CAAC,yBAAyB,YAAY,EAAE,CAAC,CAAA;IAEjD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,CAAA;AAC9D,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Re-fetch the state-level open-data sources (NY/TX/DE/OR notaries, IA contractors, WA health
7
+ * providers, HI lobbyists). Reproducible recovery if `$MAILWOMAN_DATA_ROOT` is lost.
8
+ *
9
+ * HI public schools is fetched separately by `mailwoman corpus fetch state-hi-schools` — its
10
+ * upstream is an XLSX workbook that requires an openpyxl-driven sheet-concatenation pre-step
11
+ * before the adapter can consume it.
12
+ *
13
+ * Each source lands in its own subdirectory of `<outRoot>/<slug>/` along with a `MANIFEST.json`
14
+ * recording origin URL + download timestamp + sha256 so downstream adapters can verify provenance.
15
+ *
16
+ * Invoke via `mailwoman corpus fetch state-sources --out-root <path>`. Uses Node's built-in fetch
17
+ * (gzip/brotli) and streaming sha256 instead of curl + sha256sum.
18
+ */
19
+ import type { BaseFetchOptions, FetchSummary } from "./download.ts";
20
+ export type FetchStateSourcesOptions = BaseFetchOptions;
21
+ export declare function fetchStateSources(options: FetchStateSourcesOptions, report?: (line: string) => void): Promise<FetchSummary>;
22
+ //# sourceMappingURL=state-sources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-sources.d.ts","sourceRoot":"","sources":["../../../../src/tools/fetch/state-sources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAGnE,MAAM,MAAM,wBAAwB,GAAG,gBAAgB,CAAA;AAsDvD,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,wBAAwB,EACjC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC7B,OAAO,CAAC,YAAY,CAAC,CAyDvB"}