@mailwoman/tiger 9.0.0 → 9.2.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/sdk/download.d.ts +3 -10
- package/out/sdk/download.d.ts.map +1 -1
- package/out/sdk/download.js +5 -22
- package/out/sdk/download.js.map +1 -1
- package/out/sdk/fetch.d.ts.map +1 -1
- package/out/sdk/fetch.js +14 -10
- package/out/sdk/fetch.js.map +1 -1
- package/out/sdk/redistricting.d.ts.map +1 -1
- package/out/sdk/redistricting.js +4 -7
- package/out/sdk/redistricting.js.map +1 -1
- package/out/sdk/state/TIGERTabulatedBlockEntity.d.ts.map +1 -1
- package/out/sdk/state/TIGERTabulatedBlockEntity.js +1 -1
- package/out/sdk/state/TIGERTabulatedBlockEntity.js.map +1 -1
- package/out/sdk/state/TIGERTractEntity.d.ts +1 -1
- package/out/sdk/state/TIGERTractEntity.d.ts.map +1 -1
- package/out/sdk/state/block-operations.d.ts +1 -1
- package/out/sdk/state/block-operations.d.ts.map +1 -1
- package/out/sdk/state/block-operations.js.map +1 -1
- package/out/tools/index.d.ts +1 -5
- package/out/tools/index.d.ts.map +1 -1
- package/out/tools/index.js +1 -5
- package/out/tools/index.js.map +1 -1
- package/out/tools/race-dots-map.d.ts.map +1 -1
- package/out/tools/race-dots-map.js +2 -1
- package/out/tools/race-dots-map.js.map +1 -1
- package/out/tools/race-dots.js +2 -2
- package/out/tools/race-dots.js.map +1 -1
- package/out/tools/serve-range.d.ts.map +1 -1
- package/out/tools/serve-range.js +2 -1
- package/out/tools/serve-range.js.map +1 -1
- package/out/tsconfig.tsbuildinfo +1 -1
- package/package.json +19 -7
- package/sdk/download.ts +5 -25
- package/sdk/fetch.ts +14 -9
- package/sdk/redistricting.ts +4 -7
- package/sdk/state/TIGERTabulatedBlockEntity.ts +2 -1
- package/sdk/state/TIGERTractEntity.ts +1 -1
- package/sdk/state/block-operations.ts +2 -1
- package/tools/index.ts +1 -5
- package/tools/race-dots-map.ts +3 -1
- package/tools/race-dots.ts +2 -2
- package/tools/serve-range.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mailwoman/tiger",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.2.0",
|
|
4
4
|
"description": "US Census TIGER/Line data processing and analysis.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"census",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
23
|
"url": "https://github.com/sister-software/mailwoman.git",
|
|
24
|
-
"directory": "tiger"
|
|
24
|
+
"directory": "packages/tiger"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"out/",
|
|
@@ -33,9 +33,16 @@
|
|
|
33
33
|
"!*.test.tsx",
|
|
34
34
|
"!**/*.test.ts",
|
|
35
35
|
"!**/*.test.tsx",
|
|
36
|
-
"!__snapshots__/"
|
|
36
|
+
"!__snapshots__/",
|
|
37
|
+
"!test/**"
|
|
37
38
|
],
|
|
38
39
|
"type": "module",
|
|
40
|
+
"imports": {
|
|
41
|
+
"#index": {
|
|
42
|
+
"types": "./out/index.d.ts",
|
|
43
|
+
"default": "./out/index.js"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
39
46
|
"exports": {
|
|
40
47
|
"./package.json": "./package.json",
|
|
41
48
|
"./sdk": {
|
|
@@ -67,18 +74,23 @@
|
|
|
67
74
|
"types": "./out/index.d.ts",
|
|
68
75
|
"default": "./out/index.js"
|
|
69
76
|
}
|
|
77
|
+
},
|
|
78
|
+
"imports": {
|
|
79
|
+
"#index": {
|
|
80
|
+
"types": "./out/index.d.ts",
|
|
81
|
+
"default": "./out/index.js"
|
|
82
|
+
}
|
|
70
83
|
}
|
|
71
84
|
},
|
|
72
85
|
"scripts": {
|
|
73
86
|
"test": "node ./out/sdk/TIGERService.test.js"
|
|
74
87
|
},
|
|
75
88
|
"dependencies": {
|
|
76
|
-
"@mailwoman/core": "9.
|
|
77
|
-
"@mailwoman/spatial": "9.
|
|
89
|
+
"@mailwoman/core": "9.2.0",
|
|
90
|
+
"@mailwoman/spatial": "9.2.0",
|
|
78
91
|
"@turf/boolean-contains": "^7.3.5",
|
|
79
92
|
"kysely": "^0.29.4",
|
|
80
|
-
"
|
|
81
|
-
"spliterator": "^6.0.1",
|
|
93
|
+
"spliterator": "^6.2.0",
|
|
82
94
|
"type-fest": "^5.8.0"
|
|
83
95
|
},
|
|
84
96
|
"devDependencies": {
|
package/sdk/download.ts
CHANGED
|
@@ -14,45 +14,25 @@
|
|
|
14
14
|
* never holds it: a web stream piped to a write stream.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { spawn } from "node:child_process"
|
|
18
17
|
import { createWriteStream, existsSync } from "node:fs"
|
|
19
18
|
import { rename } from "node:fs/promises"
|
|
20
19
|
import { Readable } from "node:stream"
|
|
21
20
|
import { pipeline } from "node:stream/promises"
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
* Run a command and resolve its stdout, rejecting with the captured stderr on a non-zero exit.
|
|
25
|
-
*
|
|
26
|
-
* Used for the `unzip -tq` integrity probe below — a shelled-out check rather than a library one because the archives
|
|
27
|
-
* are large and `unzip -t` streams them without materializing the contents.
|
|
28
|
-
*/
|
|
29
|
-
export function runCapture(cmd: string, args: string[]): Promise<string> {
|
|
30
|
-
return new Promise((resolve, reject) => {
|
|
31
|
-
const child = spawn(cmd, args)
|
|
32
|
-
let out = ""
|
|
33
|
-
let err = ""
|
|
34
|
-
child.stdout.on("data", (d) => (out += d))
|
|
35
|
-
child.stderr.on("data", (d) => (err += d))
|
|
36
|
-
child.on("error", reject)
|
|
37
|
-
|
|
38
|
-
child.on("close", (code) =>
|
|
39
|
-
code === 0 ? resolve(out) : reject(new Error(`${cmd} exited ${code}: ${err.slice(0, 500)}`))
|
|
40
|
-
)
|
|
41
|
-
})
|
|
42
|
-
}
|
|
22
|
+
import { verifyZipIntegrity } from "@mailwoman/core/fs/zip"
|
|
43
23
|
|
|
44
24
|
/**
|
|
45
25
|
* Download `url` to `dest` unless a VALID copy is already there. Returns `true` when the cache was reused, `false` when
|
|
46
26
|
* a download happened.
|
|
47
27
|
*
|
|
48
|
-
* "Valid" means
|
|
49
|
-
* plausible-looking file that fails only much later, inside ogr2ogr. Writes to a `.tmp` sibling and
|
|
50
|
-
* is never a partial archive.
|
|
28
|
+
* "Valid" means every member's CRC-32 checks out — an existence check alone is not enough, because an interrupted
|
|
29
|
+
* download leaves a plausible-looking file that fails only much later, inside ogr2ogr. Writes to a `.tmp` sibling and
|
|
30
|
+
* renames, so `dest` is never a partial archive.
|
|
51
31
|
*/
|
|
52
32
|
export async function downloadIfNeeded(url: string, dest: string): Promise<boolean> {
|
|
53
33
|
if (existsSync(dest)) {
|
|
54
34
|
try {
|
|
55
|
-
await
|
|
35
|
+
await verifyZipIntegrity(dest)
|
|
56
36
|
|
|
57
37
|
return true
|
|
58
38
|
} catch {
|
package/sdk/fetch.ts
CHANGED
|
@@ -25,19 +25,18 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { spawn } from "node:child_process"
|
|
28
|
-
import {
|
|
29
|
-
import { mkdir, rename } from "node:fs/promises"
|
|
28
|
+
import { mkdir } from "node:fs/promises"
|
|
30
29
|
import { dirname, join } from "node:path"
|
|
31
30
|
import { DatabaseSync } from "node:sqlite"
|
|
32
|
-
import { Readable } from "node:stream"
|
|
33
|
-
import { pipeline } from "node:stream/promises"
|
|
34
31
|
|
|
32
|
+
import { APIClient, pluckResponseData } from "@mailwoman/core/api"
|
|
33
|
+
import { extractZipEntries } from "@mailwoman/core/fs/zip"
|
|
35
34
|
import { DatabaseClient } from "@mailwoman/core/kysley/client"
|
|
36
35
|
import { tryParsingJSON } from "@mailwoman/core/objects"
|
|
37
36
|
import { mailwomanDataRoot } from "@mailwoman/core/utils"
|
|
38
37
|
import { TextSpliterator } from "spliterator"
|
|
39
38
|
|
|
40
|
-
import { downloadIfNeeded
|
|
39
|
+
import { downloadIfNeeded } from "./download.ts"
|
|
41
40
|
import type { TIGERBlockTable, TIGERDatabase, TIGERPlaceTable, TIGERStreetTable } from "./schema.ts"
|
|
42
41
|
import { initializeTIGERSchema, TIGER_PRAGMAS } from "./schema.ts"
|
|
43
42
|
|
|
@@ -175,10 +174,11 @@ function buildRow(level: TIGERFetchLevel, p: Record<string, unknown>, geometry:
|
|
|
175
174
|
* Scrape the ADDRFEAT directory listing for a state's county FIPS codes.
|
|
176
175
|
*/
|
|
177
176
|
async function discoverCounties(state: string, vintage: number): Promise<string[]> {
|
|
178
|
-
|
|
177
|
+
// The LISTING only — a small HTML index. The per-county archives below stay on raw `fetch`, streaming to disk.
|
|
178
|
+
const html = await new APIClient({ displayName: "tiger-listing", retry: true })
|
|
179
|
+
.fetch<string>({ url: `${CENSUS_HOST}/geo/tiger/TIGER${vintage}/ADDRFEAT/`, responseType: "text" })
|
|
180
|
+
.then(pluckResponseData)
|
|
179
181
|
|
|
180
|
-
if (!res.ok) throw new Error(`HTTP ${res.status} listing ADDRFEAT for vintage ${vintage}`)
|
|
181
|
-
const html = await res.text()
|
|
182
182
|
const re = new RegExp(`tl_${vintage}_${state}(\\d{3})_addrfeat\\.zip`, "g")
|
|
183
183
|
const counties = new Set<string>()
|
|
184
184
|
|
|
@@ -221,6 +221,11 @@ export async function* fetchTIGER(options: FetchTIGEROptions): AsyncGenerator<Fe
|
|
|
221
221
|
const kdb = new DatabaseClient<TIGERDatabase>({ database: db })
|
|
222
222
|
await initializeTIGERSchema(kdb)
|
|
223
223
|
|
|
224
|
+
/**
|
|
225
|
+
* `level` picks the table AND the row shape, but nothing ties them at the type level: {@link buildRow} returns the
|
|
226
|
+
* union and Kysely needs the arm's concrete row. The narrowings below are sound only because `buildRow` switches on
|
|
227
|
+
* the SAME `level` — keep the two switches in step, or a row shape reaches the wrong table.
|
|
228
|
+
*/
|
|
224
229
|
const insertBatch = async (rows: Row[]): Promise<void> => {
|
|
225
230
|
if (level === "tabblock20") {
|
|
226
231
|
await kdb
|
|
@@ -270,7 +275,7 @@ export async function* fetchTIGER(options: FetchTIGEROptions): AsyncGenerator<Fe
|
|
|
270
275
|
const cached = await downloadIfNeeded(url, zipPath)
|
|
271
276
|
yield { phase: "download", file: zipName, cached }
|
|
272
277
|
|
|
273
|
-
await
|
|
278
|
+
await extractZipEntries(zipPath, cacheDir)
|
|
274
279
|
const layer = `tl_${vintage}_${unit}_${level}`
|
|
275
280
|
const shpPath = join(cacheDir, layer + ".shp")
|
|
276
281
|
yield { phase: "extract", file: layer + ".shp" }
|
package/sdk/redistricting.ts
CHANGED
|
@@ -16,20 +16,17 @@
|
|
|
16
16
|
* https://www2.census.gov/programs-surveys/decennial/2020/data/01-Redistricting_File--PL_94-171/
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
20
|
-
import { createWriteStream, existsSync } from "node:fs"
|
|
21
|
-
import { mkdir, rename } from "node:fs/promises"
|
|
19
|
+
import { mkdir } from "node:fs/promises"
|
|
22
20
|
import { dirname, join } from "node:path"
|
|
23
21
|
import { DatabaseSync } from "node:sqlite"
|
|
24
|
-
import { Readable } from "node:stream"
|
|
25
|
-
import { pipeline } from "node:stream/promises"
|
|
26
22
|
|
|
23
|
+
import { extractZipEntries } from "@mailwoman/core/fs/zip"
|
|
27
24
|
import { DatabaseClient } from "@mailwoman/core/kysley/client"
|
|
28
25
|
import { mailwomanDataRoot } from "@mailwoman/core/utils"
|
|
29
26
|
import { TextSpliterator } from "spliterator"
|
|
30
27
|
|
|
31
28
|
import { AdminLevel1CodeToAbbreviation, StateName, type AdminLevel1Code } from "../state.ts"
|
|
32
|
-
import { downloadIfNeeded
|
|
29
|
+
import { downloadIfNeeded } from "./download.ts"
|
|
33
30
|
import { initializeTIGERSchema, TIGER_PRAGMAS, type PLBlockTable, type TIGERDatabase } from "./schema.ts"
|
|
34
31
|
|
|
35
32
|
const REDISTRICTING_BASE =
|
|
@@ -148,7 +145,7 @@ export async function* fetchRedistricting(
|
|
|
148
145
|
const cached = await downloadIfNeeded(url, zipPath)
|
|
149
146
|
yield { phase: "download", file: zipName, cached }
|
|
150
147
|
|
|
151
|
-
await
|
|
148
|
+
await extractZipEntries(zipPath, cacheDir)
|
|
152
149
|
const geoPath = join(cacheDir, `${fileAbbr}geo${vintage}.pl`)
|
|
153
150
|
const seg1Path = join(cacheDir, `${fileAbbr}00001${vintage}.pl`)
|
|
154
151
|
yield { phase: "extract", file: `${fileAbbr}geo${vintage}.pl` }
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { GeometryLiteral, MultiPolygonLiteral } from "@mailwoman/spatial"
|
|
8
8
|
import { wellKnownGeometryToGeoJSON } from "@mailwoman/spatial/sdk"
|
|
9
|
+
|
|
9
10
|
import type {
|
|
10
11
|
FIPSStateCode,
|
|
11
12
|
FIPSBlockGeoID,
|
|
@@ -13,7 +14,7 @@ import type {
|
|
|
13
14
|
TIGERBlockFeature,
|
|
14
15
|
TIGERBlockFeatureCollection,
|
|
15
16
|
TIGERTabulatedBlockProperties,
|
|
16
|
-
} from "
|
|
17
|
+
} from "#index"
|
|
17
18
|
|
|
18
19
|
export interface StateBlockIntersectionCriteria {
|
|
19
20
|
stateCode: FIPSStateCode
|
package/tools/index.ts
CHANGED
|
@@ -3,11 +3,7 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
* TIGER operator
|
|
7
|
-
* `race-dots-map` (see the 2026-07-09 scripts→Pastel spec). No argv, no `process.exit`: commands
|
|
8
|
-
* own parsing, rendering, and exit codes. Heavy deps (`@turf/boolean-contains`,
|
|
9
|
-
* `@protomaps/basemaps`) are lazy-imported inside their entry fns. NOT `sdk/` — that submodule
|
|
10
|
-
* means data acquisition.
|
|
6
|
+
* TIGER operator tool exports.
|
|
11
7
|
*/
|
|
12
8
|
|
|
13
9
|
export * from "./race-dots.ts"
|
package/tools/race-dots-map.ts
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
|
|
20
20
|
import { writeFileSync } from "node:fs"
|
|
21
21
|
|
|
22
|
+
import { tempRootPath } from "@mailwoman/core/utils"
|
|
23
|
+
|
|
22
24
|
/**
|
|
23
25
|
* Options for {@linkcode raceDotsMap}.
|
|
24
26
|
*/
|
|
@@ -104,7 +106,7 @@ export async function raceDotsMap(
|
|
|
104
106
|
report?: (line: string) => void
|
|
105
107
|
): Promise<RaceDotsMapResult> {
|
|
106
108
|
const PMTILES_URL = options.pmtilesURL || "http://localhost:8899/race-dots-oc.pmtiles"
|
|
107
|
-
const OUT = options.out || "
|
|
109
|
+
const OUT = options.out || tempRootPath("race-dots-oc.html")
|
|
108
110
|
const PER = options.per ?? 5
|
|
109
111
|
const PER_PHRASE = PER === 1 ? "one dot is one person" : `one dot ≈ ${PER} people`
|
|
110
112
|
const TITLE = options.title || `Race in Orange County, CA — ${PER_PHRASE} (2020 Census)`
|
package/tools/race-dots.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { createWriteStream } from "node:fs"
|
|
|
25
25
|
import { DatabaseSync } from "node:sqlite"
|
|
26
26
|
|
|
27
27
|
import { tryParsingJSON } from "@mailwoman/core/objects"
|
|
28
|
-
import { dataRootPath } from "@mailwoman/core/utils"
|
|
28
|
+
import { dataRootPath, tempRootPath } from "@mailwoman/core/utils"
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Attempts to place a dot inside its polygon by rejection sampling before giving up on it.
|
|
@@ -112,7 +112,7 @@ export async function raceDots(
|
|
|
112
112
|
report?: (line: string) => void
|
|
113
113
|
): Promise<RaceDotsResult> {
|
|
114
114
|
const DB = options.db || dataRootPath("tiger", "tiger-oc.db")
|
|
115
|
-
const OUT = options.out || "
|
|
115
|
+
const OUT = options.out || tempRootPath("race-dots.ndjson")
|
|
116
116
|
const PER = options.per ?? 10 // people represented by one dot
|
|
117
117
|
const LAYER = options.layer || "dots"
|
|
118
118
|
|
package/tools/serve-range.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { createReadStream, statSync, type Stats } from "node:fs"
|
|
|
15
15
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"
|
|
16
16
|
import { extname, join, normalize } from "node:path"
|
|
17
17
|
|
|
18
|
+
import { mailwomanTempRoot } from "@mailwoman/core/utils"
|
|
19
|
+
|
|
18
20
|
/**
|
|
19
21
|
* Options for {@linkcode serveWithRangeSupport}.
|
|
20
22
|
*/
|
|
@@ -55,7 +57,7 @@ export function serveWithRangeSupport(
|
|
|
55
57
|
options: ServeRangeOptions = {},
|
|
56
58
|
report?: (line: string) => void
|
|
57
59
|
): Promise<RangeServer> {
|
|
58
|
-
const dir = options.dir ||
|
|
60
|
+
const dir = options.dir || mailwomanTempRoot()
|
|
59
61
|
const port = options.port ?? 8899
|
|
60
62
|
|
|
61
63
|
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|