@mailwoman/tiger 8.0.0 → 8.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.
Files changed (76) hide show
  1. package/constants.ts +12 -12
  2. package/county.ts +5 -2
  3. package/files.ts +4 -1
  4. package/geoid.ts +11 -9
  5. package/out/constants.d.ts +5 -5
  6. package/out/constants.d.ts.map +1 -1
  7. package/out/constants.js +0 -5
  8. package/out/constants.js.map +1 -1
  9. package/out/county.d.ts +5 -2
  10. package/out/county.d.ts.map +1 -1
  11. package/out/county.js +3 -2
  12. package/out/county.js.map +1 -1
  13. package/out/files.d.ts +1 -1
  14. package/out/files.d.ts.map +1 -1
  15. package/out/files.js +0 -1
  16. package/out/files.js.map +1 -1
  17. package/out/geoid.d.ts +14 -16
  18. package/out/geoid.d.ts.map +1 -1
  19. package/out/geoid.js +1 -2
  20. package/out/geoid.js.map +1 -1
  21. package/out/sdk/fetch.d.ts +18 -6
  22. package/out/sdk/fetch.d.ts.map +1 -1
  23. package/out/sdk/fetch.js +11 -5
  24. package/out/sdk/fetch.js.map +1 -1
  25. package/out/sdk/redistricting.d.ts +18 -6
  26. package/out/sdk/redistricting.d.ts.map +1 -1
  27. package/out/sdk/redistricting.js +12 -6
  28. package/out/sdk/redistricting.js.map +1 -1
  29. package/out/sdk/schema.d.ts +14 -7
  30. package/out/sdk/schema.d.ts.map +1 -1
  31. package/out/sdk/schema.js +4 -0
  32. package/out/sdk/schema.js.map +1 -1
  33. package/out/sdk/state/TIGERTractEntity.d.ts +1 -1
  34. package/out/sdk/state/TIGERTractEntity.d.ts.map +1 -1
  35. package/out/sdk/state/TIGERTractEntity.js +1 -1
  36. package/out/sdk/state/TIGERTractEntity.js.map +1 -1
  37. package/out/sdk/state/block-operations.d.ts +1 -1
  38. package/out/sdk/state/block-operations.d.ts.map +1 -1
  39. package/out/sdk/state/block-operations.js +0 -1
  40. package/out/sdk/state/block-operations.js.map +1 -1
  41. package/out/state.d.ts +343 -113
  42. package/out/state.d.ts.map +1 -1
  43. package/out/state.js +174 -57
  44. package/out/state.js.map +1 -1
  45. package/out/tabulation-block.d.ts +3 -3
  46. package/out/tabulation-block.d.ts.map +1 -1
  47. package/out/tabulation-block.js +1 -3
  48. package/out/tabulation-block.js.map +1 -1
  49. package/out/tools/race-dots-map.d.ts +30 -10
  50. package/out/tools/race-dots-map.d.ts.map +1 -1
  51. package/out/tools/race-dots-map.js +6 -2
  52. package/out/tools/race-dots-map.js.map +1 -1
  53. package/out/tools/race-dots.d.ts +24 -8
  54. package/out/tools/race-dots.d.ts.map +1 -1
  55. package/out/tools/race-dots.js +15 -5
  56. package/out/tools/race-dots.js.map +1 -1
  57. package/out/tools/serve-range.d.ts +12 -4
  58. package/out/tools/serve-range.d.ts.map +1 -1
  59. package/out/tools/serve-range.js.map +1 -1
  60. package/out/tract.d.ts +2 -2
  61. package/out/tract.d.ts.map +1 -1
  62. package/out/tract.js +1 -2
  63. package/out/tract.js.map +1 -1
  64. package/out/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +3 -3
  66. package/sdk/fetch.ts +36 -11
  67. package/sdk/redistricting.ts +39 -12
  68. package/sdk/schema.ts +21 -7
  69. package/sdk/state/TIGERTractEntity.ts +1 -1
  70. package/sdk/state/block-operations.ts +7 -6
  71. package/state.ts +175 -57
  72. package/tabulation-block.ts +3 -3
  73. package/tools/race-dots-map.ts +35 -11
  74. package/tools/race-dots.ts +52 -12
  75. package/tools/serve-range.ts +16 -4
  76. package/tract.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mailwoman/tiger",
3
- "version": "8.0.0",
3
+ "version": "8.2.0",
4
4
  "description": "US Census TIGER/Line data processing and analysis.",
5
5
  "keywords": [
6
6
  "census",
@@ -73,8 +73,8 @@
73
73
  "test": "node ./out/sdk/TIGERService.test.js"
74
74
  },
75
75
  "dependencies": {
76
- "@mailwoman/core": "8.0.0",
77
- "@mailwoman/spatial": "8.0.0",
76
+ "@mailwoman/core": "8.2.0",
77
+ "@mailwoman/spatial": "8.2.0",
78
78
  "@turf/boolean-contains": "^7.3.5",
79
79
  "kysely": "^0.29.4",
80
80
  "shapefile-parser": "^1.0.3",
package/sdk/fetch.ts CHANGED
@@ -52,6 +52,7 @@ const LEVEL_DIR: Record<TIGERFetchLevel, string> = {
52
52
  place: "PLACE",
53
53
  addrfeat: "ADDRFEAT",
54
54
  }
55
+
55
56
  const LEVEL_TABLE: Record<TIGERFetchLevel, keyof TIGERDatabase> = {
56
57
  tabblock20: "tabblock20",
57
58
  place: "tiger_places",
@@ -59,21 +60,33 @@ const LEVEL_TABLE: Record<TIGERFetchLevel, keyof TIGERDatabase> = {
59
60
  }
60
61
 
61
62
  export interface FetchTIGEROptions {
62
- /** Two-digit state FIPS, e.g. `"06"`. */
63
+ /**
64
+ * Two-digit state FIPS, e.g. `"06"`.
65
+ */
63
66
  stateFIPS: string
64
- /** TIGER level. Default `tabblock20`. */
67
+ /**
68
+ * TIGER level. Default `tabblock20`.
69
+ */
65
70
  level?: TIGERFetchLevel
66
- /** Vintage. Default 2020 for blocks (matches the 2020 P.L.), 2024 for place/addrfeat (current). */
71
+ /**
72
+ * Vintage. Default 2020 for blocks (matches the 2020 P.L.), 2024 for place/addrfeat (current).
73
+ */
67
74
  vintage?: number
68
75
  /**
69
76
  * Output SQLite path. Default `<dataRoot>/tiger/tiger.db` (the name the corpus `tiger` adapter reads).
70
77
  */
71
78
  outPath?: string
72
- /** Download cache + default output root. */
79
+ /**
80
+ * Download cache + default output root.
81
+ */
73
82
  dataRoot?: string
74
- /** Optional three-digit county FIPS filter (blocks only — addrfeat is already per-county). */
83
+ /**
84
+ * Optional three-digit county FIPS filter (blocks only — addrfeat is already per-county).
85
+ */
75
86
  county?: string
76
- /** Rows per insert. Default 1000. */
87
+ /**
88
+ * Rows per insert. Default 1000.
89
+ */
77
90
  batchSize?: number
78
91
  }
79
92
 
@@ -88,7 +101,9 @@ export interface FetchTIGERResult {
88
101
  inserted: number
89
102
  }
90
103
 
91
- /** The isp-nexus column map for `tabblock20`. Geometry rides along implicitly. */
104
+ /**
105
+ * The isp-nexus column map for `tabblock20`. Geometry rides along implicitly.
106
+ */
92
107
  function blockSelectSQL(layer: string, county?: string): string {
93
108
  const where = county ? ` WHERE COUNTYFP20 = '${county}'` : ""
94
109
 
@@ -162,6 +177,7 @@ function runCapture(cmd: string, args: string[]): Promise<string> {
162
177
  child.stdout.on("data", (d) => (out += d))
163
178
  child.stderr.on("data", (d) => (err += d))
164
179
  child.on("error", reject)
180
+
165
181
  child.on("close", (code) =>
166
182
  code === 0 ? resolve(out) : reject(new Error(`${cmd} exited ${code}: ${err.slice(0, 500)}`))
167
183
  )
@@ -178,6 +194,7 @@ async function downloadIfNeeded(url: string, dest: string): Promise<boolean> {
178
194
  // corrupt cache — re-download
179
195
  }
180
196
  }
197
+
181
198
  const tmp = dest + ".tmp"
182
199
  const res = await fetch(url, { redirect: "follow" })
183
200
 
@@ -188,7 +205,9 @@ async function downloadIfNeeded(url: string, dest: string): Promise<boolean> {
188
205
  return false
189
206
  }
190
207
 
191
- /** Scrape the ADDRFEAT directory listing for a state's county FIPS codes. */
208
+ /**
209
+ * Scrape the ADDRFEAT directory listing for a state's county FIPS codes.
210
+ */
192
211
  async function discoverCounties(state: string, vintage: number): Promise<string[]> {
193
212
  const res = await fetch(`${CENSUS_HOST}/geo/tiger/TIGER${vintage}/ADDRFEAT/`, { redirect: "follow" })
194
213
 
@@ -201,7 +220,7 @@ async function discoverCounties(state: string, vintage: number): Promise<string[
201
220
  counties.add(m[1]!)
202
221
  }
203
222
 
204
- return [...counties].sort()
223
+ return [...counties].toSorted()
205
224
  }
206
225
 
207
226
  /**
@@ -227,7 +246,7 @@ export async function* fetchTIGER(options: FetchTIGEROptions): AsyncGenerator<Fe
227
246
  // Source units: one (per-state) for block/place; one per county for addrfeat.
228
247
  const geoCodes = level === "addrfeat" ? await discoverCounties(state, vintage) : [""]
229
248
 
230
- if (level === "addrfeat" && geoCodes.length === 0) {
249
+ if (level === "addrfeat" && !geoCodes.length) {
231
250
  throw new Error(`No ADDRFEAT counties found for state ${state} vintage ${vintage}`)
232
251
  }
233
252
 
@@ -267,6 +286,7 @@ export async function* fetchTIGER(options: FetchTIGEROptions): AsyncGenerator<Fe
267
286
 
268
287
  let inserted = 0
269
288
  let batch: Row[] = []
289
+
270
290
  const flush = async () => {
271
291
  if (!batch.length) return
272
292
  const rows = batch
@@ -303,9 +323,13 @@ export async function* fetchTIGER(options: FetchTIGEROptions): AsyncGenerator<Fe
303
323
  ],
304
324
  { stdio: ["ignore", "pipe", "pipe"] }
305
325
  )
326
+
306
327
  let stderr = ""
307
328
  child.stderr.on("data", (d) => (stderr += d))
308
- const exited = new Promise<number>((resolve) => child.on("close", (code) => resolve(code ?? 0)))
329
+
330
+ const exited = new Promise<number>((resolve) => {
331
+ child.on("close", (code) => resolve(code ?? 0))
332
+ })
309
333
 
310
334
  // GeoJSONSeq is line-delimited; keep the per-line `JSON.parse` in a try/catch so a malformed
311
335
  // record is tolerated (skipped), not thrown.
@@ -329,6 +353,7 @@ export async function* fetchTIGER(options: FetchTIGEROptions): AsyncGenerator<Fe
329
353
  yield { phase: "load", inserted, total: 0 }
330
354
  }
331
355
  }
356
+
332
357
  await flush()
333
358
 
334
359
  const code = await exited
@@ -33,18 +33,26 @@ import { initializeTIGERSchema, TIGER_PRAGMAS, type PLBlockTable, type TIGERData
33
33
 
34
34
  const REDISTRICTING_BASE =
35
35
  "https://www2.census.gov/programs-surveys/decennial/2020/data/01-Redistricting_File--PL_94-171"
36
+
36
37
  const DEFAULT_DATA_ROOT = mailwomanDataRoot()
37
38
 
38
- // P.L. 94-171 (2020) pipe-delimited field offsets (0-based).
39
- // Geographic header: …|SUMLEV(2)|…|LOGRECNO(7)|GEOID(8)|GEOCODE(9)|… — GEOCODE is the bare 15-char
40
- // block FIPS (matches TIGER GEOID20); SUMLEV 750 = tabulation block.
39
+ /**
40
+ * P.L. 94-171 (2020) pipe-delimited field offsets (0-based). Geographic header:
41
+ * …|SUMLEV(2)|…|LOGRECNO(7)|GEOID(8)|GEOCODE(9)|… — GEOCODE is the bare 15-char block FIPS (matches TIGER GEOID20);
42
+ * SUMLEV 750 = tabulation block.
43
+ */
41
44
  const GEO_SUMLEV = 2
42
45
  const GEO_LOGRECNO = 7
43
46
  const GEO_GEOCODE = 9
44
- // Segment 1: FILEID|STUSAB|CHARITER|CIFSN|LOGRECNO(4)| P1×71 | P2×73. P0020001 is at index 76.
47
+ /**
48
+ * Segment 1: FILEID|STUSAB|CHARITER|CIFSN|LOGRECNO(4)| P1×71 | P2×73. P0020001 is at index 76.
49
+ */
45
50
  const SEG_LOGRECNO = 4
46
51
  const P2 = (fieldNo: number) => 76 + (fieldNo - 1)
47
- // The eight P2 categories that partition the total (P0020001), in `pl_block` column order.
52
+
53
+ /**
54
+ * The eight P2 categories that partition the total (P0020001), in `pl_block` column order.
55
+ */
48
56
  const CATEGORY_INDEX = {
49
57
  pop_total: P2(1),
50
58
  hispanic: P2(2), // Hispanic or Latino (any race)
@@ -58,17 +66,29 @@ const CATEGORY_INDEX = {
58
66
  } as const
59
67
 
60
68
  export interface FetchRedistrictingOptions {
61
- /** Two-digit state FIPS, e.g. `"06"`. */
69
+ /**
70
+ * Two-digit state FIPS, e.g. `"06"`.
71
+ */
62
72
  stateFIPS: string
63
- /** Decennial vintage. Default 2020 (the only P.L. 94-171 release this parses). */
73
+ /**
74
+ * Decennial vintage. Default 2020 (the only P.L. 94-171 release this parses).
75
+ */
64
76
  vintage?: number
65
- /** Output SQLite path. Default `<dataRoot>/tiger/tiger.db` (same DB as `fetchTIGER`). */
77
+ /**
78
+ * Output SQLite path. Default `<dataRoot>/tiger/tiger.db` (same DB as `fetchTIGER`).
79
+ */
66
80
  outPath?: string
67
- /** Download cache + default output root. */
81
+ /**
82
+ * Download cache + default output root.
83
+ */
68
84
  dataRoot?: string
69
- /** Optional three-digit county FIPS filter, e.g. `"059"`. */
85
+ /**
86
+ * Optional three-digit county FIPS filter, e.g. `"059"`.
87
+ */
70
88
  county?: string
71
- /** Rows per insert. Default 2000. */
89
+ /**
90
+ * Rows per insert. Default 2000.
91
+ */
72
92
  batchSize?: number
73
93
  }
74
94
 
@@ -92,6 +112,7 @@ function runCapture(cmd: string, args: string[]): Promise<string> {
92
112
  child.stdout.on("data", (d) => (out += d))
93
113
  child.stderr.on("data", (d) => (err += d))
94
114
  child.on("error", reject)
115
+
95
116
  child.on("close", (code) =>
96
117
  code === 0 ? resolve(out) : reject(new Error(`${cmd} exited ${code}: ${err.slice(0, 500)}`))
97
118
  )
@@ -108,6 +129,7 @@ async function downloadIfNeeded(url: string, dest: string): Promise<boolean> {
108
129
  // corrupt cache — re-download
109
130
  }
110
131
  }
132
+
111
133
  const tmp = dest + ".tmp"
112
134
  const res = await fetch(url, { redirect: "follow" })
113
135
 
@@ -145,7 +167,7 @@ export async function* fetchRedistricting(
145
167
 
146
168
  if (!abbr) throw new Error(`Unknown state FIPS "${state}"`)
147
169
  const stateName = StateName[abbr as keyof typeof StateName]
148
- const dirName = stateName.replace(/ /g, "_")
170
+ const dirName = stateName.replaceAll(" ", "_")
149
171
  const fileAbbr = abbr.toLowerCase()
150
172
 
151
173
  const cacheDir = join(dataRoot, "census", "redistricting", String(vintage), state)
@@ -169,6 +191,7 @@ export async function* fetchRedistricting(
169
191
  // Pass 1: header → LOGRECNO → GEOID for the blocks we want.
170
192
  const prefix = options.county ? state + options.county : state
171
193
  const logToGeoid = new Map<string, string>()
194
+
172
195
  await eachLine(geoPath, (line) => {
173
196
  const f = line.split("|")
174
197
 
@@ -178,6 +201,7 @@ export async function* fetchRedistricting(
178
201
  if (!geoid.startsWith(prefix)) return
179
202
  logToGeoid.set(f[GEO_LOGRECNO] ?? "", geoid)
180
203
  })
204
+
181
205
  const total = logToGeoid.size
182
206
  yield { phase: "header", blocks: total }
183
207
 
@@ -195,6 +219,7 @@ export async function* fetchRedistricting(
195
219
 
196
220
  let inserted = 0
197
221
  let batch: PLBlockTable[] = []
222
+
198
223
  const flush = async () => {
199
224
  if (!batch.length) return
200
225
  const rows = batch
@@ -210,6 +235,7 @@ export async function* fetchRedistricting(
210
235
  const geoid = logToGeoid.get(f[SEG_LOGRECNO] ?? "")
211
236
 
212
237
  if (!geoid) continue
238
+
213
239
  batch.push({
214
240
  GEOID: geoid,
215
241
  pop_total: Number(f[CATEGORY_INDEX.pop_total] ?? 0),
@@ -228,6 +254,7 @@ export async function* fetchRedistricting(
228
254
  yield { phase: "load", inserted, total }
229
255
  }
230
256
  }
257
+
231
258
  await flush()
232
259
 
233
260
  yield { phase: "load", inserted, total }
package/sdk/schema.ts CHANGED
@@ -10,9 +10,10 @@
10
10
  * back with `JSON.parse`).
11
11
  */
12
12
 
13
- import { sql, type Generated, type Kysely } from "kysely"
13
+ import { sql, type Kysely } from "kysely"
14
+
15
+ export { type Generated } from "kysely"
14
16
 
15
- /** Kysely row type for `tabblock20`. Geometry is a GeoJSON string. */
16
17
  export interface TIGERBlockTable {
17
18
  GEOID: string
18
19
  state_code: string
@@ -47,7 +48,9 @@ export interface PLBlockTable {
47
48
  multi: number
48
49
  }
49
50
 
50
- /** Kysely row type for `tiger_streets` (ADDRFEAT — named street segments + ZIPs, per county). */
51
+ /**
52
+ * Kysely row type for `tiger_streets` (ADDRFEAT — named street segments + ZIPs, per county).
53
+ */
51
54
  export interface TIGERStreetTable {
52
55
  linearid: string
53
56
  fullname: string
@@ -56,7 +59,9 @@ export interface TIGERStreetTable {
56
59
  statefp: string
57
60
  }
58
61
 
59
- /** Kysely row type for `tiger_places` (PLACE — incorporated/census places, per state). */
62
+ /**
63
+ * Kysely row type for `tiger_places` (PLACE — incorporated/census places, per state).
64
+ */
60
65
  export interface TIGERPlaceTable {
61
66
  geoid: string
62
67
  name: string
@@ -66,7 +71,9 @@ export interface TIGERPlaceTable {
66
71
  classfp: string | null
67
72
  }
68
73
 
69
- /** The TIGER database schema, for `new DatabaseClient<TIGERDatabase>(...)`. */
74
+ /**
75
+ * The TIGER database schema, for `new DatabaseClient<TIGERDatabase>(...)`.
76
+ */
70
77
  export interface TIGERDatabase {
71
78
  tabblock20: TIGERBlockTable
72
79
  pl_block: PLBlockTable
@@ -74,8 +81,9 @@ export interface TIGERDatabase {
74
81
  tiger_places: TIGERPlaceTable
75
82
  }
76
83
 
77
- /** Marker so callers can opt into `Generated` columns later without importing kysely here. */
78
- export type { Generated }
84
+ /**
85
+ * Marker so callers can opt into `Generated` columns later without importing kysely here.
86
+ */
79
87
 
80
88
  /**
81
89
  * Build-tuning PRAGMAs, run raw before any table is created (`page_size`/`auto_vacuum` only take effect on an empty DB,
@@ -139,18 +147,21 @@ export async function initializeTIGERSchema(db: Kysely<TIGERDatabase>): Promise<
139
147
  // No index on GEOID alone — it's the PRIMARY KEY, which already carries a unique index. The prior
140
148
  // schema's idx_tabblock20_geoid duplicated that for nothing (double insert cost, double footprint).
141
149
  await db.schema.createIndex("idx_tabblock20_state_code").ifNotExists().on("tabblock20").column("state_code").execute()
150
+
142
151
  await db.schema
143
152
  .createIndex("idx_tabblock20_state_county")
144
153
  .ifNotExists()
145
154
  .on("tabblock20")
146
155
  .columns(["state_code", "county_code"])
147
156
  .execute()
157
+
148
158
  await db.schema
149
159
  .createIndex("idx_tabblock20_state_county_tract")
150
160
  .ifNotExists()
151
161
  .on("tabblock20")
152
162
  .columns(["state_code", "county_code", "tract_code"])
153
163
  .execute()
164
+
154
165
  await db.schema.createIndex("idx_tabblock20_population").ifNotExists().on("tabblock20").column("population").execute()
155
166
 
156
167
  await db.schema
@@ -180,7 +191,9 @@ export async function initializeTIGERSchema(db: Kysely<TIGERDatabase>): Promise<
180
191
  .addColumn("zipr", "text")
181
192
  .addColumn("statefp", "text", (c) => c.notNull())
182
193
  .execute()
194
+
183
195
  await db.schema.createIndex("idx_tiger_streets_statefp").ifNotExists().on("tiger_streets").column("statefp").execute()
196
+
184
197
  await db.schema
185
198
  .createIndex("idx_tiger_streets_linearid")
186
199
  .ifNotExists()
@@ -198,6 +211,7 @@ export async function initializeTIGERSchema(db: Kysely<TIGERDatabase>): Promise<
198
211
  .addColumn("namelsad", "text")
199
212
  .addColumn("classfp", "text")
200
213
  .execute()
214
+
201
215
  await db.schema.createIndex("idx_tiger_places_statefp").ifNotExists().on("tiger_places").column("statefp").execute()
202
216
  await db.schema.createIndex("idx_tiger_places_geoid").ifNotExists().on("tiger_places").column("geoid").execute()
203
217
  }
@@ -4,7 +4,7 @@
4
4
  * @author Teffen Ellis, et al.
5
5
  */
6
6
 
7
- import { AdminLevel1Code, type FIPSCountyCode, type FIPSTractCode } from "@mailwoman/tiger"
7
+ import type { AdminLevel1Code, FIPSCountyCode, FIPSTractCode } from "@mailwoman/tiger"
8
8
 
9
9
  /**
10
10
  * @internal
@@ -6,13 +6,13 @@
6
6
 
7
7
  import type { GeometryLiteral, MultiPolygonLiteral } from "@mailwoman/spatial"
8
8
  import { wellKnownGeometryToGeoJSON } from "@mailwoman/spatial/sdk"
9
- import {
9
+ import type {
10
10
  FIPSStateCode,
11
- type FIPSBlockGeoID,
12
- type FIPSTractCode,
13
- type TIGERBlockFeature,
14
- type TIGERBlockFeatureCollection,
15
- type TIGERTabulatedBlockProperties,
11
+ FIPSBlockGeoID,
12
+ FIPSTractCode,
13
+ TIGERBlockFeature,
14
+ TIGERBlockFeatureCollection,
15
+ TIGERTabulatedBlockProperties,
16
16
  } from "@mailwoman/tiger"
17
17
 
18
18
  export interface StateBlockIntersectionCriteria {
@@ -57,6 +57,7 @@ export function parseTIGERBlockFromRow({ serializedGeometry, ...properties }: TI
57
57
  */
58
58
  export function parseTIGERBlockFeatureCollectionFromRows(rows: TIGERBlockRow[]): TIGERBlockFeatureCollection {
59
59
  const features = rows.map(parseTIGERBlockFromRow)
60
+
60
61
  const featureCollection: TIGERBlockFeatureCollection = {
61
62
  type: "FeatureCollection",
62
63
  features,