@mailwoman/soil 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.
Files changed (97) hide show
  1. package/README.md +221 -0
  2. package/lib/index.ts +474 -0
  3. package/lib/schema.ts +493 -0
  4. package/lib/scripts/ingest-chunk.ts +47 -0
  5. package/lib/sdk/acquire.ts +124 -0
  6. package/lib/sdk/build-soil.ts +778 -0
  7. package/lib/sdk/cell-tiers.ts +310 -0
  8. package/lib/sdk/cells.ts +224 -0
  9. package/lib/sdk/client.ts +273 -0
  10. package/lib/sdk/download.ts +161 -0
  11. package/lib/sdk/index.ts +20 -0
  12. package/lib/sdk/ingest-chunk.ts +182 -0
  13. package/lib/sdk/ingest.ts +271 -0
  14. package/lib/sdk/measure-resolutions.ts +81 -0
  15. package/lib/sdk/reduce.ts +375 -0
  16. package/lib/sdk/survey-area.ts +467 -0
  17. package/lib/sdk/tabular.ts +296 -0
  18. package/lib/sdk/verify.ts +369 -0
  19. package/lib/test-kit.ts +255 -0
  20. package/lib/vocabulary.ts +241 -0
  21. package/out/index.d.ts +199 -0
  22. package/out/index.d.ts.map +1 -0
  23. package/out/index.js +255 -0
  24. package/out/index.js.map +1 -0
  25. package/out/schema.d.ts +369 -0
  26. package/out/schema.d.ts.map +1 -0
  27. package/out/schema.js +186 -0
  28. package/out/schema.js.map +1 -0
  29. package/out/scripts/ingest-chunk.d.ts +11 -0
  30. package/out/scripts/ingest-chunk.d.ts.map +1 -0
  31. package/out/scripts/ingest-chunk.js +42 -0
  32. package/out/scripts/ingest-chunk.js.map +1 -0
  33. package/out/sdk/acquire.d.ts +60 -0
  34. package/out/sdk/acquire.d.ts.map +1 -0
  35. package/out/sdk/acquire.js +74 -0
  36. package/out/sdk/acquire.js.map +1 -0
  37. package/out/sdk/build-soil.d.ts +197 -0
  38. package/out/sdk/build-soil.d.ts.map +1 -0
  39. package/out/sdk/build-soil.js +434 -0
  40. package/out/sdk/build-soil.js.map +1 -0
  41. package/out/sdk/cell-tiers.d.ts +47 -0
  42. package/out/sdk/cell-tiers.d.ts.map +1 -0
  43. package/out/sdk/cell-tiers.js +194 -0
  44. package/out/sdk/cell-tiers.js.map +1 -0
  45. package/out/sdk/cells.d.ts +118 -0
  46. package/out/sdk/cells.d.ts.map +1 -0
  47. package/out/sdk/cells.js +140 -0
  48. package/out/sdk/cells.js.map +1 -0
  49. package/out/sdk/client.d.ts +120 -0
  50. package/out/sdk/client.d.ts.map +1 -0
  51. package/out/sdk/client.js +214 -0
  52. package/out/sdk/client.js.map +1 -0
  53. package/out/sdk/download.d.ts +84 -0
  54. package/out/sdk/download.d.ts.map +1 -0
  55. package/out/sdk/download.js +117 -0
  56. package/out/sdk/download.js.map +1 -0
  57. package/out/sdk/index.d.ts +20 -0
  58. package/out/sdk/index.d.ts.map +1 -0
  59. package/out/sdk/index.js +20 -0
  60. package/out/sdk/index.js.map +1 -0
  61. package/out/sdk/ingest-chunk.d.ts +73 -0
  62. package/out/sdk/ingest-chunk.d.ts.map +1 -0
  63. package/out/sdk/ingest-chunk.js +98 -0
  64. package/out/sdk/ingest-chunk.js.map +1 -0
  65. package/out/sdk/ingest.d.ts +132 -0
  66. package/out/sdk/ingest.d.ts.map +1 -0
  67. package/out/sdk/ingest.js +163 -0
  68. package/out/sdk/ingest.js.map +1 -0
  69. package/out/sdk/measure-resolutions.d.ts +39 -0
  70. package/out/sdk/measure-resolutions.d.ts.map +1 -0
  71. package/out/sdk/measure-resolutions.js +52 -0
  72. package/out/sdk/measure-resolutions.js.map +1 -0
  73. package/out/sdk/reduce.d.ts +122 -0
  74. package/out/sdk/reduce.d.ts.map +1 -0
  75. package/out/sdk/reduce.js +266 -0
  76. package/out/sdk/reduce.js.map +1 -0
  77. package/out/sdk/survey-area.d.ts +105 -0
  78. package/out/sdk/survey-area.d.ts.map +1 -0
  79. package/out/sdk/survey-area.js +346 -0
  80. package/out/sdk/survey-area.js.map +1 -0
  81. package/out/sdk/tabular.d.ts +106 -0
  82. package/out/sdk/tabular.d.ts.map +1 -0
  83. package/out/sdk/tabular.js +194 -0
  84. package/out/sdk/tabular.js.map +1 -0
  85. package/out/sdk/verify.d.ts +135 -0
  86. package/out/sdk/verify.d.ts.map +1 -0
  87. package/out/sdk/verify.js +221 -0
  88. package/out/sdk/verify.js.map +1 -0
  89. package/out/test-kit.d.ts +80 -0
  90. package/out/test-kit.d.ts.map +1 -0
  91. package/out/test-kit.js +228 -0
  92. package/out/test-kit.js.map +1 -0
  93. package/out/vocabulary.d.ts +179 -0
  94. package/out/vocabulary.d.ts.map +1 -0
  95. package/out/vocabulary.js +215 -0
  96. package/out/vocabulary.js.map +1 -0
  97. package/package.json +276 -0
@@ -0,0 +1,346 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * One survey area's attributes, its own metadata, and its mapped footprint.
7
+ *
8
+ * THE FOOTPRINT IS THE SURVEY-AREA OUTLINE, NEVER THE UNION OF THE RATED POLYGONS. `NOTCOM`,
9
+ * access-denied and `NOTPUB` map units are INSIDE the footprint and carry no rating, so a footprint taken
10
+ * from the rated set would report them as unmapped when the authority has declared exactly what they are.
11
+ * The archive ships the outline as its own shapefile — `soilsa_a_<areasymbol>.shp`, one feature — which is
12
+ * why this layer never has to reconstruct it.
13
+ *
14
+ * THE REFRESH DATE IS NOT THE SURVEY DATE, AND CONFLATING THEM IS THE CURRENCY LIE THIS FILE EXISTS TO
15
+ * PREVENT. `IA153` carries `saverest` 2025-09-09 and version 28, and the FGDC lineage inside the same
16
+ * archive cites `Soil Survey of Polk County, Iowa`, 1:15,840, **1960**. The dataset's own
17
+ * time-period-of-content runs 1998-09-22 to 2025-09-09, so a consumer reading that as survey currency
18
+ * reads it wrong by sixty-five years. Both dates are stored, apart, with the title the older one came
19
+ * from so it is checkable rather than assertible.
20
+ *
21
+ * TWO SCALES, ALSO DIFFERENT FACTS. `legend.projectscale` is 12,000 for `IA153` — the scale the map units
22
+ * were digitized at. The 1960 source citation's own `srcscale` is 15,840 — the scale the ground was
23
+ * walked at. Storing one as the other would answer the enlargement caveat's question wrongly.
24
+ *
25
+ * THE LICENCE IS CHECKED PER SURVEY AREA, against the `useconst` element of the metadata that area ships.
26
+ * An area whose use constraints no longer say "This is public information" is a licence change, and a
27
+ * build that absorbed one would ship an artifact under terms nobody checked. The text is boilerplate
28
+ * repeated across SSURGO, which is why asserting it is cheap and why a change in it is loud.
29
+ */
30
+ import { parseJSONStrict } from "@mailwoman/core/objects";
31
+ import { runFile } from "@mailwoman/core/process";
32
+ import { domainCodes, readDeclaredDomains, readTable, readTabularDictionary } from "#sdk/tabular";
33
+ import { COINTERP_OVERALL_RULE_DEPTH, farmlandScope, NCCPI_V3_RULE_NAME, SSURGO_NO_MAPPING_NAMES, SSURGO_NO_MAPPING_SYMBOLS, SSURGO_PUBLIC_INFORMATION_SENTENCE, } from "#vocabulary";
34
+ /**
35
+ * The declared domains this layer validates against, and stores.
36
+ *
37
+ * `capability_class` is shared by `nirrcapcl`, `irrcapcl` and `muaggatt.niccdcd`, which is why one domain covers three
38
+ * columns.
39
+ */
40
+ export const STORED_DOMAINS = [
41
+ "capability_class",
42
+ "capability_subclass",
43
+ "farmland_classification",
44
+ "component_kind",
45
+ "mapunit_kind",
46
+ "mapunit_status",
47
+ ];
48
+ /**
49
+ * Read one survey area's tabular export.
50
+ *
51
+ * @throws {Error} When the metadata's use constraints no longer carry the public-information sentence, when a `Choice`
52
+ * column holds a value outside the authority's own declared domain, or when the export declares no legend row.
53
+ */
54
+ export async function readSurveyAreaAttributes(tabularDirectory, areaSymbol) {
55
+ const dictionary = await readTabularDictionary(tabularDirectory);
56
+ const domains = await readDeclaredDomains(tabularDirectory);
57
+ const catalog = await readTable(tabularDirectory, dictionary, "sacatalog", [
58
+ "areasymbol",
59
+ "areaname",
60
+ "saverest",
61
+ "saversion",
62
+ "fgdcmetadata",
63
+ ]);
64
+ if (catalog.rows.length !== 1) {
65
+ throw new Error(`soil survey area: ${areaSymbol}'s sacatlog.txt holds ${catalog.rows.length} records, expected 1 — a survey-area archive describes exactly one survey area`);
66
+ }
67
+ const catalogRow = catalog.rows[0];
68
+ const metadata = readFGDCMetadata(catalogRow.fgdcmetadata, areaSymbol);
69
+ const legend = await readTable(tabularDirectory, dictionary, "legend", [
70
+ "areasymbol",
71
+ "areaname",
72
+ "areaacres",
73
+ "projectscale",
74
+ ]);
75
+ if (!legend.rows.length) {
76
+ throw new Error(`soil survey area: ${areaSymbol}'s legend.txt holds no record, so the published area is unknown`);
77
+ }
78
+ const legendRow = legend.rows.find((row) => row.areasymbol === areaSymbol) ?? legend.rows[0];
79
+ const mapUnitRows = await readTable(tabularDirectory, dictionary, "mapunit", [
80
+ "mukey",
81
+ "musym",
82
+ "muname",
83
+ "mukind",
84
+ "mustatus",
85
+ "farmlndcl",
86
+ ]);
87
+ const aggregate = await readTable(tabularDirectory, dictionary, "muaggatt", ["mukey", "niccdcd", "niccdcdpct"]);
88
+ const aggregateByMukey = new Map(aggregate.rows.map((row) => [row.mukey, row]));
89
+ const componentRows = await readTable(tabularDirectory, dictionary, "component", [
90
+ "cokey",
91
+ "mukey",
92
+ "comppct_r",
93
+ "compname",
94
+ "compkind",
95
+ "nirrcapcl",
96
+ "nirrcapscl",
97
+ "irrcapcl",
98
+ "irrcapscl",
99
+ ]);
100
+ const nccpiByCokey = await readNCCPI(tabularDirectory, dictionary);
101
+ const classCodes = domainCodes(domains, "capability_class");
102
+ const subclassCodes = domainCodes(domains, "capability_subclass");
103
+ const componentKinds = domainCodes(domains, "component_kind");
104
+ const farmlandCodes = domainCodes(domains, "farmland_classification");
105
+ const mapUnitKinds = domainCodes(domains, "mapunit_kind");
106
+ const componentsByMukey = new Map();
107
+ const components = componentRows.rows.map((row) => {
108
+ assertDeclared(classCodes, row.nirrcapcl, "capability_class", `component ${row.cokey}.nirrcapcl`);
109
+ assertDeclared(classCodes, row.irrcapcl, "capability_class", `component ${row.cokey}.irrcapcl`);
110
+ assertDeclared(subclassCodes, row.nirrcapscl, "capability_subclass", `component ${row.cokey}.nirrcapscl`);
111
+ assertDeclared(subclassCodes, row.irrcapscl, "capability_subclass", `component ${row.cokey}.irrcapscl`);
112
+ assertDeclared(componentKinds, row.compkind, "component_kind", `component ${row.cokey}.compkind`);
113
+ componentsByMukey.set(row.mukey, (componentsByMukey.get(row.mukey) ?? 0) + 1);
114
+ return {
115
+ cokey: row.cokey,
116
+ mukey: row.mukey,
117
+ // A blank `comppct_r` is a component with no declared weight. Zero is the truthful reading — it contributes
118
+ // nothing to a weighted share — and it is recorded rather than dropped, so the component still appears.
119
+ comppct_r: row.comppct_r ? Number(row.comppct_r) : 0,
120
+ compname: nullable(row.compname),
121
+ compkind: nullable(row.compkind),
122
+ nirrcapcl: nullable(row.nirrcapcl),
123
+ nirrcapscl: nullable(row.nirrcapscl),
124
+ irrcapcl: nullable(row.irrcapcl),
125
+ irrcapscl: nullable(row.irrcapscl),
126
+ nccpi_v3: nccpiByCokey.get(row.cokey) ?? null,
127
+ };
128
+ });
129
+ const mapUnits = mapUnitRows.rows.map((row) => {
130
+ assertDeclared(farmlandCodes, row.farmlndcl, "farmland_classification", `map unit ${row.mukey}.farmlndcl`);
131
+ assertDeclared(mapUnitKinds, row.mukind, "mapunit_kind", `map unit ${row.mukey}.mukind`);
132
+ const aggregated = aggregateByMukey.get(row.mukey);
133
+ assertDeclared(classCodes, aggregated?.niccdcd, "capability_class", `map unit ${row.mukey}.niccdcd`);
134
+ return {
135
+ mukey: row.mukey,
136
+ areasymbol: areaSymbol,
137
+ musym: row.musym,
138
+ muname: row.muname,
139
+ mukind: nullable(row.mukind),
140
+ mustatus: nullable(row.mustatus),
141
+ farmlndcl: nullable(row.farmlndcl),
142
+ farmland_scope: farmlandScope(row.farmlndcl),
143
+ niccdcd: nullable(aggregated?.niccdcd),
144
+ niccdcdpct: aggregated?.niccdcdpct ? Number(aggregated.niccdcdpct) : null,
145
+ no_mapping: isNoMapping(row.musym, row.muname, componentsByMukey.get(row.mukey) ?? 0) ? 1 : 0,
146
+ };
147
+ });
148
+ return {
149
+ areasymbol: areaSymbol,
150
+ areaname: legendRow.areaname || catalogRow.areaname,
151
+ saverest: metadata.publicationDate,
152
+ saversion: catalogRow.saversion ? Number(catalogRow.saversion) : null,
153
+ surveySourceDate: metadata.oldestSourceDate,
154
+ surveySourceTitle: metadata.oldestSourceTitle,
155
+ sourceScale: metadata.oldestSourceScale,
156
+ mappingScale: legendRow.projectscale ? Number(legendRow.projectscale) : null,
157
+ areaAcres: legendRow.areaacres ? Number(legendRow.areaacres) : null,
158
+ mapUnits,
159
+ components,
160
+ domains: domains.filter((member) => STORED_DOMAINS.includes(member.domain)),
161
+ };
162
+ }
163
+ /**
164
+ * A polygon the authority drew with no soil mapping behind it.
165
+ *
166
+ * Three signals rather than one, because the source encodes the same fact three ways and each on its own has a gap: the
167
+ * symbol (`NOTCOM`, `NOTPUB`), the name (`Area not surveyed, access denied`), and the structural case of a map unit
168
+ * carrying NO components at all. A map unit with no components has nothing to rate whatever it is called, and reading
169
+ * it as "rated nothing" rather than "no mapping" would put it in `unrated_share` — a claim that the survey looked and
170
+ * declined, when it did not look.
171
+ */
172
+ function isNoMapping(musym, muname, componentCount) {
173
+ if (SSURGO_NO_MAPPING_SYMBOLS.has(musym.toUpperCase()))
174
+ return true;
175
+ if (SSURGO_NO_MAPPING_NAMES.has(muname.trim().toLowerCase()))
176
+ return true;
177
+ return componentCount === 0;
178
+ }
179
+ /**
180
+ * Refuse a value outside the authority's own declared domain.
181
+ *
182
+ * An unknown code is a source-schema change, which is the event a reader most needs to hear about; coercing it to a
183
+ * nearest neighbour or to NULL converts "the source changed" into "there is nothing here". A BLANK is not a violation:
184
+ * NULL is a real state in every one of these columns and means something specific — for `nirrcapcl` it means the survey
185
+ * did not rate the component, which is not class 8.
186
+ */
187
+ function assertDeclared(declared, value, domain, where) {
188
+ if (!value)
189
+ return;
190
+ if (declared.has(value))
191
+ return;
192
+ throw new Error(`soil survey area: ${where} holds ${JSON.stringify(value)}, which is not in the authority's declared ${domain} domain (${declared.size} members, read from the archive's own msdomdet.txt) — an unknown code is a source-schema change, and coercing it would turn "the source changed" into "there is nothing here"`);
193
+ }
194
+ function nullable(value) {
195
+ return value || null;
196
+ }
197
+ /**
198
+ * The NCCPI v3.0 overall index per component.
199
+ *
200
+ * `cointerp` is the largest table in the export — 157,063 rows for `IA153`, read in 0.36 s — and the overall rule is
201
+ * one row per component at {@link COINTERP_OVERALL_RULE_DEPTH}: 369 of 369 components on `IA153`, of which 327 carry a
202
+ * value. Sub-rules at greater depths are the submodels (corn, soybeans, small grains, cotton), which this layer does
203
+ * not carry.
204
+ */
205
+ async function readNCCPI(tabularDirectory, dictionary) {
206
+ const rows = await readTable(tabularDirectory, dictionary, "cointerp", [
207
+ "cokey",
208
+ "mrulename",
209
+ "ruledepth",
210
+ "interphr",
211
+ ]);
212
+ const byCokey = new Map();
213
+ for (const row of rows.rows) {
214
+ if (row.mrulename !== NCCPI_V3_RULE_NAME)
215
+ continue;
216
+ if (row.ruledepth !== COINTERP_OVERALL_RULE_DEPTH)
217
+ continue;
218
+ if (!row.interphr)
219
+ continue;
220
+ byCokey.set(row.cokey, Number(row.interphr));
221
+ }
222
+ return byCokey;
223
+ }
224
+ /**
225
+ * Read the metadata NRCS ships inside the archive.
226
+ *
227
+ * Targeted extraction rather than a general XML parse, and NOT for want of a parser — `@mailwoman/core` ships
228
+ * `htmlparser2`. A parser RECOVERS an unclosed element by giving it the rest of the document as its content, and the
229
+ * two values below that throw would then stamp the artifact with that content instead. {@link elementText} answers
230
+ * `undefined` for an element it cannot read, which is what makes the throw reachable. Every value this reader cannot
231
+ * find is reported as `null` EXCEPT the publication date and the licence sentence, which throw — those two decide the
232
+ * artifact's vintage and whether it may be shipped at all, and neither has a safe default.
233
+ *
234
+ * @throws {Error} When the metadata carries no publication date, or its use constraints no longer carry the
235
+ * public-information sentence.
236
+ */
237
+ export function readFGDCMetadata(xml, areaSymbol) {
238
+ const useConstraints = elementText(xml, "useconst");
239
+ if (!useConstraints?.includes(SSURGO_PUBLIC_INFORMATION_SENTENCE)) {
240
+ throw new Error(`soil survey area: ${areaSymbol}'s FGDC use constraints do not carry ${JSON.stringify(SSURGO_PUBLIC_INFORMATION_SENTENCE)} — that sentence is the grant this layer ships on, so a survey area without it must not be built into a distributable artifact`);
241
+ }
242
+ const publicationDate = elementText(xml, "pubdate");
243
+ if (!publicationDate) {
244
+ throw new Error(`soil survey area: ${areaSymbol}'s FGDC metadata carries no publication date — stamping the artifact with a guessed vintage would give it a version that means nothing`);
245
+ }
246
+ const sources = readSourceCitations(xml);
247
+ const oldest = sources.toSorted((left, right) => (left.date < right.date ? -1 : 1))[0];
248
+ return {
249
+ publicationDate: normalizeFGDCDate(publicationDate),
250
+ oldestSourceDate: oldest ? normalizeFGDCDate(oldest.date) : null,
251
+ oldestSourceTitle: oldest?.title ?? null,
252
+ oldestSourceScale: oldest?.scale ?? null,
253
+ };
254
+ }
255
+ /**
256
+ * The lineage's source citations: what the polygons rest on, and when each was made.
257
+ */
258
+ function readSourceCitations(xml) {
259
+ const citations = [];
260
+ for (const body of elementBlocks(xml, "srcinfo")) {
261
+ // `caldate` for a single date, `begdate` for a range. A range's END is when the source stopped being collected;
262
+ // its BEGINNING is when the ground was first looked at, which is the fact this layer is carrying.
263
+ const date = elementText(body, "caldate") ?? elementText(body, "begdate");
264
+ if (!date)
265
+ continue;
266
+ const scale = elementText(body, "srcscale");
267
+ citations.push({
268
+ date: date.trim(),
269
+ title: (elementText(body, "title") ?? "").replaceAll(/\s+/gu, " ").trim(),
270
+ scale: scale ? Number(scale) : null,
271
+ });
272
+ }
273
+ return citations;
274
+ }
275
+ /**
276
+ * The text of the first `<name>` element, whitespace left alone.
277
+ *
278
+ * INDEX SCANS RATHER THAN A REGEX, AND THAT IS A CORRECTNESS CHOICE RATHER THAN A SPEED ONE. The obvious form — ``new
279
+ * RegExp(`<${name}>([\\s\\S]*?)</${name}>`)`` — backtracks polynomially on a document whose opening tag has no closing
280
+ * partner: the lazy run re-scans to the end from every candidate start. The input here is a 43,251-character document
281
+ * that arrived over the network inside a downloaded archive, so "a malformed one cannot happen" is not a claim this
282
+ * reader gets to make. Two `indexOf` calls answer the same question in one pass.
283
+ */
284
+ function elementText(xml, name) {
285
+ const open = `<${name}>`;
286
+ const start = xml.indexOf(open);
287
+ if (start === -1)
288
+ return undefined;
289
+ const from = start + open.length;
290
+ const end = xml.indexOf(`</${name}>`, from);
291
+ // An element with no closing tag is unreadable, not empty — the same answer an absent element gets, because both
292
+ // mean the value could not be read rather than that it is blank.
293
+ return end === -1 ? undefined : xml.slice(from, end);
294
+ }
295
+ /**
296
+ * Every `<name>` element's inner text, in document order. The repeating counterpart of {@link elementText}, and linear
297
+ * for the same reason.
298
+ */
299
+ function elementBlocks(xml, name) {
300
+ const open = `<${name}>`;
301
+ const close = `</${name}>`;
302
+ const blocks = [];
303
+ let cursor = 0;
304
+ for (;;) {
305
+ const start = xml.indexOf(open, cursor);
306
+ if (start === -1)
307
+ return blocks;
308
+ const from = start + open.length;
309
+ const end = xml.indexOf(close, from);
310
+ if (end === -1)
311
+ return blocks;
312
+ blocks.push(xml.slice(from, end));
313
+ cursor = end + close.length;
314
+ }
315
+ }
316
+ /**
317
+ * FGDC dates arrive as `YYYY` or `YYYYMMDD`. Both are kept as they are meant — a bare year is a bare year, and padding
318
+ * it to January 1 would invent a precision the citation does not claim.
319
+ */
320
+ function normalizeFGDCDate(value) {
321
+ const trimmed = value.trim();
322
+ const matched = /^(\d{4})(\d{2})(\d{2})$/u.exec(trimmed);
323
+ return matched ? `${matched[1]}-${matched[2]}-${matched[3]}` : trimmed;
324
+ }
325
+ /**
326
+ * Read the survey area's own outline shapefile as a GeoJSON geometry.
327
+ *
328
+ * @throws {Error} When the shapefile holds anything other than exactly one feature. Taking the first of several would
329
+ * silently choose which ground the coverage claim is about.
330
+ */
331
+ export async function readSurveyAreaOutline(shapefilePath) {
332
+ const { stdout } = await runFile("ogr2ogr", ["-f", "GeoJSON", "/vsistdout/", "-t_srs", "EPSG:4326", shapefilePath], {
333
+ maxBuffer: 256 * 1024 * 1024,
334
+ });
335
+ const collection = parseJSONStrict(stdout);
336
+ const features = collection.features ?? [];
337
+ if (features.length !== 1) {
338
+ throw new Error(`soil survey area: ${shapefilePath} holds ${features.length} features, expected exactly 1 — a survey area publishes one outline, and taking the first of several would silently choose which ground the coverage claim is about`);
339
+ }
340
+ const geometry = features[0].geometry;
341
+ if (!geometry) {
342
+ throw new Error(`soil survey area: ${shapefilePath}'s single feature carries no geometry`);
343
+ }
344
+ return geometry;
345
+ }
346
+ //# sourceMappingURL=survey-area.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"survey-area.js","sourceRoot":"","sources":["../../lib/sdk/survey-area.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The pipe-delimited NASIS export inside a survey-area archive: which file holds which table, which
7
+ * column sits at which position, and the authority's own declared domains.
8
+ *
9
+ * THE FILES CARRY NO HEADER AND THE ARCHIVE SHIPS THE SCHEMA. `mstab.txt` maps a logical table name to
10
+ * the file base name that holds it (`component` → `comp.txt`, `sacatalog` → `sacatlog.txt` — neither is
11
+ * guessable), and `mstabcol.txt` gives every column's ordinal position. So the reader looks the positions
12
+ * up rather than hard-coding them, and {@link readTable} THROWS on a requested column the shipped
13
+ * dictionary does not declare. A reader that quietly returned `undefined` for a renamed column would turn
14
+ * "the source changed" into "there is none of it", at exactly the measurement boundary where that lie
15
+ * costs the most.
16
+ *
17
+ * QUOTE HANDLING IS NOT OPTIONAL HERE, AND THE MEASUREMENT determines the result. `sacatlog.txt` holds 594 newline
18
+ * bytes and exactly ONE record: its `fgdcmetadata` column carries a 43,251-character XML document with
19
+ * embedded newlines. `mstabcol.txt` — the column dictionary itself — holds 913 newlines and 865 records.
20
+ * A line-splitting reader gets 594 malformed rows from a one-row file, every one of them well-formed
21
+ * enough to keep going.
22
+ *
23
+ * THE DECLARED DOMAINS COME OUT OF THE ARCHIVE TOO, which is stronger than transcribing them.
24
+ * `msdomdet.txt` carries every `Choice` column's members WITH the authority's own prose definition —
25
+ * capability classes 1 through 8, subclasses `c`/`e`/`s`/`w`, the 28 conditional farmland
26
+ * classifications, the six component kinds. The layer stores them and validates against them.
27
+ */
28
+ import { type PathBuilderLike } from "path-ts";
29
+ /**
30
+ * One parsed record: the raw column strings, in the authority's declared order.
31
+ */
32
+ export type TabularRow = ReadonlyArray<string>;
33
+ /**
34
+ * The archive's own description of itself: table → file, and table → column positions.
35
+ */
36
+ export interface TabularDictionary {
37
+ /**
38
+ * Logical table name → file base name, from `mstab.txt`.
39
+ */
40
+ files: ReadonlyMap<string, string>;
41
+ /**
42
+ * Logical table name → column name → zero-based position, from `mstabcol.txt`.
43
+ */
44
+ columns: ReadonlyMap<string, ReadonlyMap<string, number>>;
45
+ }
46
+ /**
47
+ * Read the archive's table and column dictionaries.
48
+ *
49
+ * @throws {Error} When either bootstrap file is missing or is not the declared width.
50
+ */
51
+ export declare function readTabularDictionary(tabularDirectory: PathBuilderLike): Promise<TabularDictionary>;
52
+ /**
53
+ * A reader over one logical table, projecting the columns a caller names.
54
+ *
55
+ * The projection is by NAME and a missing name throws, which is the whole point: this is the shape that produced the
56
+ * repository's worst measurement bugs, where a silently dropped column read downstream as an empty world.
57
+ */
58
+ export interface TabularTable {
59
+ /**
60
+ * One record per row, already projected to the requested columns.
61
+ */
62
+ rows: ReadonlyArray<Record<string, string>>;
63
+ /**
64
+ * How many records the file held, before projection.
65
+ */
66
+ recordCount: number;
67
+ }
68
+ /**
69
+ * Read a logical SSURGO table, projecting `wanted` columns.
70
+ *
71
+ * @throws {Error} When the archive declares no file for the table, when a requested column is not in the shipped
72
+ * dictionary, or when a record is narrower than the position a requested column sits at.
73
+ */
74
+ export declare function readTable(tabularDirectory: PathBuilderLike, dictionary: TabularDictionary, table: string, wanted: ReadonlyArray<string>): Promise<TabularTable>;
75
+ /**
76
+ * One declared domain member, with the authority's own definition.
77
+ */
78
+ export interface DomainMember {
79
+ domain: string;
80
+ code: string;
81
+ definition: string;
82
+ sequence: number;
83
+ }
84
+ /**
85
+ * Read the authority's declared domains out of the archive.
86
+ *
87
+ * @throws {Error} When the file is not the declared width.
88
+ */
89
+ export declare function readDeclaredDomains(tabularDirectory: PathBuilderLike): Promise<DomainMember[]>;
90
+ /**
91
+ * The declared members of one domain, keyed by code.
92
+ */
93
+ export declare function domainCodes(members: ReadonlyArray<DomainMember>, domain: string): Set<string>;
94
+ /**
95
+ * `M/D/YYYY H:MM:SS` (and the `MM/DD/YYYY HH:MM:SS` the tabular export writes) to an ISO date.
96
+ *
97
+ * The two channels spell the same instant differently — Soil Data Access answers `9/9/2025 1:57:25 PM` and the shipped
98
+ * `sacatlog.txt` writes `09/09/2025 13:57:25` — and the download URL needs `2025-09-09`. Parsing to a date rather than
99
+ * slicing the string is what makes both channels agree.
100
+ *
101
+ * @throws {Error} When the value is not one of those shapes. A freshness date guessed wrong asks the download host for
102
+ * a file that does not exist, and the host answers 400 rather than 404, which reads as a bad request rather than a
103
+ * bad date.
104
+ */
105
+ export declare function saverestToISODate(value: string): string;
106
+ //# sourceMappingURL=tabular.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabular.d.ts","sourceRoot":"","sources":["../../lib/sdk/tabular.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,EAAQ,KAAK,eAAe,EAAE,MAAM,SAAS,CAAA;AAGpD;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,CAAA;AA2B9C;;GAEG;AACH,MAAM,WAAW,iBAAiB;IACjC;;OAEG;IACH,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC;;OAEG;IACH,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACzD;AAuBD;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,gBAAgB,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA+BzG;AAYD;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC5B;;OAEG;IACH,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC3C;;OAEG;IACH,WAAW,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAC9B,gBAAgB,EAAE,eAAe,EACjC,UAAU,EAAE,iBAAiB,EAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,GAC3B,OAAO,CAAC,YAAY,CAAC,CA+CvB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;CAChB;AASD;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,gBAAgB,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAWpG;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAU7F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYvD"}