@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.
- package/README.md +221 -0
- package/lib/index.ts +474 -0
- package/lib/schema.ts +493 -0
- package/lib/scripts/ingest-chunk.ts +47 -0
- package/lib/sdk/acquire.ts +124 -0
- package/lib/sdk/build-soil.ts +778 -0
- package/lib/sdk/cell-tiers.ts +310 -0
- package/lib/sdk/cells.ts +224 -0
- package/lib/sdk/client.ts +273 -0
- package/lib/sdk/download.ts +161 -0
- package/lib/sdk/index.ts +20 -0
- package/lib/sdk/ingest-chunk.ts +182 -0
- package/lib/sdk/ingest.ts +271 -0
- package/lib/sdk/measure-resolutions.ts +81 -0
- package/lib/sdk/reduce.ts +375 -0
- package/lib/sdk/survey-area.ts +467 -0
- package/lib/sdk/tabular.ts +296 -0
- package/lib/sdk/verify.ts +369 -0
- package/lib/test-kit.ts +255 -0
- package/lib/vocabulary.ts +241 -0
- package/out/index.d.ts +199 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +255 -0
- package/out/index.js.map +1 -0
- package/out/schema.d.ts +369 -0
- package/out/schema.d.ts.map +1 -0
- package/out/schema.js +186 -0
- package/out/schema.js.map +1 -0
- package/out/scripts/ingest-chunk.d.ts +11 -0
- package/out/scripts/ingest-chunk.d.ts.map +1 -0
- package/out/scripts/ingest-chunk.js +42 -0
- package/out/scripts/ingest-chunk.js.map +1 -0
- package/out/sdk/acquire.d.ts +60 -0
- package/out/sdk/acquire.d.ts.map +1 -0
- package/out/sdk/acquire.js +74 -0
- package/out/sdk/acquire.js.map +1 -0
- package/out/sdk/build-soil.d.ts +197 -0
- package/out/sdk/build-soil.d.ts.map +1 -0
- package/out/sdk/build-soil.js +434 -0
- package/out/sdk/build-soil.js.map +1 -0
- package/out/sdk/cell-tiers.d.ts +47 -0
- package/out/sdk/cell-tiers.d.ts.map +1 -0
- package/out/sdk/cell-tiers.js +194 -0
- package/out/sdk/cell-tiers.js.map +1 -0
- package/out/sdk/cells.d.ts +118 -0
- package/out/sdk/cells.d.ts.map +1 -0
- package/out/sdk/cells.js +140 -0
- package/out/sdk/cells.js.map +1 -0
- package/out/sdk/client.d.ts +120 -0
- package/out/sdk/client.d.ts.map +1 -0
- package/out/sdk/client.js +214 -0
- package/out/sdk/client.js.map +1 -0
- package/out/sdk/download.d.ts +84 -0
- package/out/sdk/download.d.ts.map +1 -0
- package/out/sdk/download.js +117 -0
- package/out/sdk/download.js.map +1 -0
- package/out/sdk/index.d.ts +20 -0
- package/out/sdk/index.d.ts.map +1 -0
- package/out/sdk/index.js +20 -0
- package/out/sdk/index.js.map +1 -0
- package/out/sdk/ingest-chunk.d.ts +73 -0
- package/out/sdk/ingest-chunk.d.ts.map +1 -0
- package/out/sdk/ingest-chunk.js +98 -0
- package/out/sdk/ingest-chunk.js.map +1 -0
- package/out/sdk/ingest.d.ts +132 -0
- package/out/sdk/ingest.d.ts.map +1 -0
- package/out/sdk/ingest.js +163 -0
- package/out/sdk/ingest.js.map +1 -0
- package/out/sdk/measure-resolutions.d.ts +39 -0
- package/out/sdk/measure-resolutions.d.ts.map +1 -0
- package/out/sdk/measure-resolutions.js +52 -0
- package/out/sdk/measure-resolutions.js.map +1 -0
- package/out/sdk/reduce.d.ts +122 -0
- package/out/sdk/reduce.d.ts.map +1 -0
- package/out/sdk/reduce.js +266 -0
- package/out/sdk/reduce.js.map +1 -0
- package/out/sdk/survey-area.d.ts +105 -0
- package/out/sdk/survey-area.d.ts.map +1 -0
- package/out/sdk/survey-area.js +346 -0
- package/out/sdk/survey-area.js.map +1 -0
- package/out/sdk/tabular.d.ts +106 -0
- package/out/sdk/tabular.d.ts.map +1 -0
- package/out/sdk/tabular.js +194 -0
- package/out/sdk/tabular.js.map +1 -0
- package/out/sdk/verify.d.ts +135 -0
- package/out/sdk/verify.d.ts.map +1 -0
- package/out/sdk/verify.js +221 -0
- package/out/sdk/verify.js.map +1 -0
- package/out/test-kit.d.ts +80 -0
- package/out/test-kit.d.ts.map +1 -0
- package/out/test-kit.js +228 -0
- package/out/test-kit.js.map +1 -0
- package/out/vocabulary.d.ts +179 -0
- package/out/vocabulary.d.ts.map +1 -0
- package/out/vocabulary.js +215 -0
- package/out/vocabulary.js.map +1 -0
- package/package.json +276 -0
package/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# `@mailwoman/soil`
|
|
2
|
+
|
|
3
|
+
USDA NRCS SSURGO soil survey as a sealed spatial layer: acquisition, the `soil.db` build, and its reader.
|
|
4
|
+
|
|
5
|
+
The layer answers one question — **what does the soil survey assign to the map unit covering this
|
|
6
|
+
location** — and it answers it as a distribution rather than as a class. That shape is not a preference;
|
|
7
|
+
it is what three measurements force.
|
|
8
|
+
|
|
9
|
+
## What it stores, and why it is a distribution
|
|
10
|
+
|
|
11
|
+
| measurement | figure |
|
|
12
|
+
| --------------------------------------------------------------------------- | -----------------------------: |
|
|
13
|
+
| national map units holding two or more components | 284,777 of 339,191 (**84.0%**) |
|
|
14
|
+
| map units where the largest component covers under half of it | 57,053 (**16.8%**) |
|
|
15
|
+
| `IA153` delineations smaller than one resolution-9 cell | 15,350 of 17,966 (**85.4%**) |
|
|
16
|
+
| NRCS's own dominant-condition share `muaggatt.niccdcdpct`, observed minimum | **2%** |
|
|
17
|
+
|
|
18
|
+
No affordable cell size removes the mixture, because the mixture is the survey's own finding: 128,499
|
|
19
|
+
map units (38.0%) are complexes, associations or undifferentiated groups, which is NRCS stating that the
|
|
20
|
+
soils are intermingled and cannot be separated at the mapping scale. NRCS itself ships its
|
|
21
|
+
dominant-condition class beside the share that class covers. `soil_capability_cell` reproduces that
|
|
22
|
+
pattern at cell grain.
|
|
23
|
+
|
|
24
|
+
**One artifact, two consumers.** A result-level observation reads `top_class` with `top_class_share`;
|
|
25
|
+
a bulk per-cell signal reads `class_shares` plus the four absence shares as one axis. One acquisition,
|
|
26
|
+
one aggregation, one set of provenance rows, and no way for the two to disagree about what the ground is.
|
|
27
|
+
|
|
28
|
+
## Four absences, and the one positive negative
|
|
29
|
+
|
|
30
|
+
An absence is never represented by a small number. Five readings a consumer can tell apart:
|
|
31
|
+
|
|
32
|
+
| reading | where it lives |
|
|
33
|
+
| ---------------------------------------------------- | -------------------------------------------------- |
|
|
34
|
+
| the survey rated this land as precluding cultivation | class `"8"` in `class_shares` — a determination |
|
|
35
|
+
| the survey did not rate it | `unrated_share` |
|
|
36
|
+
| the rating does not apply to it (rock, water) | `notrateable_share` |
|
|
37
|
+
| the polygon exists, the soil mapping does not | `nodata_share` (`NOTCOM`, `NOTPUB`, access denied) |
|
|
38
|
+
| there is no survey here at all | **no `layer_coverage` row, and no summary row** |
|
|
39
|
+
|
|
40
|
+
Class 8 is a determination and is a class share like any other. Folding it in with the others produces a
|
|
41
|
+
well-formed wrong answer, and 67,547 national components carry it. The irrigated rating makes the point
|
|
42
|
+
again at larger scale: `irrcapcl` is NULL on 85.1% of national components because it is populated only
|
|
43
|
+
where irrigation is a considered use, so it is carried and never reduced.
|
|
44
|
+
|
|
45
|
+
`other_share` carries the truncated minority tail, so the five shares always sum to 1 and a reader can
|
|
46
|
+
see how much was folded away rather than inferring it from a gap. `mapped_share` says how much of the
|
|
47
|
+
cell any delineation covers at all — without it, a survey-area edge cell's unmapped remainder would
|
|
48
|
+
silently deflate every class share.
|
|
49
|
+
|
|
50
|
+
## The resolution, measured
|
|
51
|
+
|
|
52
|
+
The index resolution is a measurement, not an argument. Measured on `IA153` — 17,966 delineations over
|
|
53
|
+
1,532.5 km², median delineation 24,863 m²:
|
|
54
|
+
|
|
55
|
+
| res | touched cells | whole | partial | partial share | whole after compaction | (cell, delineation) pairs | mean delineations/cell | top class under half |
|
|
56
|
+
| --- | ------------: | -----: | ------: | ------------: | ---------------------: | ------------------------: | ---------------------: | -------------------: |
|
|
57
|
+
| 7 | 348 | 0 | 348 | **100.0%** | 0 | 24,073 | 69.18 | **44.4%** |
|
|
58
|
+
| 8 | 2,237 | 9 | 2,228 | **99.6%** | 9 | 36,569 | 16.35 | **39.8%** |
|
|
59
|
+
| 9 | 15,136 | 369 | 14,767 | **97.6%** | 315 | 80,956 | 5.35 | **30.7%** |
|
|
60
|
+
| 10 | 104,508 | 13,691 | 90,817 | **86.9%** | 11,537 | 268,408 | 2.57 | **18.2%** |
|
|
61
|
+
|
|
62
|
+
**The `partial` share inverts against the flood layer, exactly as the survey predicted, and the inversion
|
|
63
|
+
is total.** Flood polygons are large against their cells, so most cells fall wholly inside one zone and
|
|
64
|
+
`compactCells` collapses long uniform interiors. Soil delineations are the opposite, so **the containment
|
|
65
|
+
index answers almost no probe on its own at any candidate resolution**, and compaction yields close to
|
|
66
|
+
nothing: at resolution 9, 369 whole cells compact to 315 — a 14.6% reduction, against a flood layer whose
|
|
67
|
+
interiors collapse by orders of magnitude. At resolution 7 it collapses zero of zero.
|
|
68
|
+
|
|
69
|
+
That is why this layer carries the reduced `soil_capability_cell` **alongside** the index rather than
|
|
70
|
+
relying on the index the way the flood layer can. The unsimplified geometry is still the truth and is
|
|
71
|
+
still what the reduction weights by; it is not what answers a probe.
|
|
72
|
+
|
|
73
|
+
**The two numbers move in opposite directions, and only one of them discriminates.** The `partial` share
|
|
74
|
+
is 87–100% at every candidate, so it cannot choose a resolution here — which is itself the finding. The
|
|
75
|
+
mixture number can, and it is the one the choice rests on.
|
|
76
|
+
|
|
77
|
+
**Resolution 9 is the choice.** It is where `poi.db` keys its rows, so a reader already holding another
|
|
78
|
+
layer's cells finds these without a conversion; its mixture share (30.7%) is well inside the range the
|
|
79
|
+
authority's own aggregation lives in; and resolution 10 costs 6.9× the cells (104,508 against 15,136 for
|
|
80
|
+
one county) to move the mixture from 30.7% to 18.2%, and leaves 5.2% of its cells carrying no class at all against
|
|
81
|
+
2.7% at resolution 9. Resolution 11 was excluded before measuring: it
|
|
82
|
+
would leave 2.1% of `IA153`'s delineations sub-cell at roughly 49× the resolution-9 cell count.
|
|
83
|
+
|
|
84
|
+
For comparison, NRCS's own map-unit-grain `niccdcdpct` reads below half on 3.3% of national map units.
|
|
85
|
+
Aggregating to a resolution-9 cell multiplies that roughly ninefold, which is the cost of the cell grain
|
|
86
|
+
stated as a number rather than as a worry.
|
|
87
|
+
|
|
88
|
+
## Acquisition
|
|
89
|
+
|
|
90
|
+
- **Soil Data Access** (`sdmdataaccess.nrcs.usda.gov/Tabular/post.rest`) — the survey-area catalogue and
|
|
91
|
+
the point-intersection check, through `APIClient`. Anonymous, no key, measured at 0.374 s for a tabular
|
|
92
|
+
answer and 1.807 s for a point intersection.
|
|
93
|
+
- **Survey-area archives** (`websoilsurvey.sc.egov.usda.gov/DSD/Download/Cache/SSA`) — file transfers on
|
|
94
|
+
raw `fetch`, streamed to disk, saying so in place.
|
|
95
|
+
|
|
96
|
+
Three measured behaviours the code is written against:
|
|
97
|
+
|
|
98
|
+
1. **Failures come back as XML, including on a timeout.** A bad column, a blocked query and a
|
|
99
|
+
server-side timeout all return an OGC `ServiceExceptionReport`, and the timeout arrives on an HTTP 200.
|
|
100
|
+
Every response is read as text and checked for the report before anything parses it as JSON.
|
|
101
|
+
2. **The download host answers `HEAD` with 405 and ignores `Range`.** A request with `Range: bytes=0-0`
|
|
102
|
+
returned HTTP 200 and transferred the whole 27,598,377 bytes. Freshness comes from
|
|
103
|
+
`sacatalog.saverest`, which is also what the archive's filename embeds. A wrong date is an HTTP **400**,
|
|
104
|
+
not a 404.
|
|
105
|
+
3. **The tabular export carries embedded newlines.** `sacatlog.txt` holds 594 newline bytes and exactly
|
|
106
|
+
ONE record, because `fgdcmetadata` is a 43,251-character XML document; `mstabcol.txt` — the column
|
|
107
|
+
dictionary itself — holds 913 newlines and 865 records. The reader is quote-aware end to end.
|
|
108
|
+
|
|
109
|
+
**The archive ships its own schema and its own vocabulary.** `mstab.txt` maps a logical table to the file
|
|
110
|
+
that holds it (`component` → `comp.txt`; neither is guessable), `mstabcol.txt` gives every column's
|
|
111
|
+
position, and `msdomdet.txt` carries each `Choice` column's declared members **with NRCS's own prose
|
|
112
|
+
definition** — capability classes 1 through 8, subclasses `c`/`e`/`s`/`w`, the 28 conditional farmland
|
|
113
|
+
classifications, the six component kinds. The layer reads its domain out of the file it ingested rather
|
|
114
|
+
than transcribing it, stores it in `soil_vocabulary`, and throws on a value outside it.
|
|
115
|
+
|
|
116
|
+
## License, and where the grant comes from
|
|
117
|
+
|
|
118
|
+
data.gov's entry carries `usa.gov/publicdomain/label/1.0/`, which redirects to a page that declines a
|
|
119
|
+
blanket grant and tells the reader to check with the agency. The agency was checked at the strongest
|
|
120
|
+
available place — **the FGDC metadata NRCS ships inside every archive** — and its use constraints say:
|
|
121
|
+
|
|
122
|
+
> This is public information and may be interpreted by organizations, agencies, units of government, or
|
|
123
|
+
> others based on needs; however, they are responsible for the appropriate application.
|
|
124
|
+
|
|
125
|
+
The build asserts that sentence is present **per survey area**. An area whose use constraints no longer
|
|
126
|
+
carry it is a license change, and a build that absorbed one would ship an artifact under terms nobody
|
|
127
|
+
checked. The acknowledgement the same metadata asks for rides in `layer_manifest.attribution`:
|
|
128
|
+
_U.S. Department of Agriculture, Natural Resources Conservation Service._
|
|
129
|
+
|
|
130
|
+
## Two dates, and they are not the same fact
|
|
131
|
+
|
|
132
|
+
`sacatalog.saverest` is the refresh. NRCS runs ONE coordinated Annual Soils Refresh each October 1, and
|
|
133
|
+
grouping the catalogue by year returns 2016: 1, 2025: 3,323, 2026: 56 — so a region's areas share a
|
|
134
|
+
vintage. **The field survey underneath is far older.** `IA153` carries a 2025-09-09 refresh over a
|
|
135
|
+
_Soil Survey of Polk County, Iowa_ published in **1960** at 1:15,840, and the dataset's own
|
|
136
|
+
time-period-of-content ends at the refresh. A consumer reading that as survey currency reads it wrong by
|
|
137
|
+
sixty-five years.
|
|
138
|
+
|
|
139
|
+
Both dates are stored per survey area, apart, with the title the older one came from so it is checkable.
|
|
140
|
+
Two scales are kept apart for the same reason: `legend.projectscale` (12,000 for `IA153`) is the scale the
|
|
141
|
+
map units were digitized at; the source citation's own `srcscale` (15,840) is the scale the ground was
|
|
142
|
+
walked at.
|
|
143
|
+
|
|
144
|
+
## What a reading may claim
|
|
145
|
+
|
|
146
|
+
> the soil survey assigns this capability class to the map unit covering this location
|
|
147
|
+
|
|
148
|
+
and never
|
|
149
|
+
|
|
150
|
+
> this land can (or cannot) be farmed.
|
|
151
|
+
|
|
152
|
+
NRCS says the second reading is wrong, in the metadata it ships: the data "do not eliminate the need for
|
|
153
|
+
onsite sampling, testing, and detailed study of specific sites for intensive uses. Thus, these data and
|
|
154
|
+
their interpretations are intended for planning purposes only." Every reading carries the product's own
|
|
155
|
+
limits for that reason.
|
|
156
|
+
|
|
157
|
+
**The farmland vocabulary is conditional, and two of its categories do not travel.** 24 of its 28 declared
|
|
158
|
+
values carry an "if" — `Prime farmland if drained`, `Prime farmland if irrigated and reclaimed of excess
|
|
159
|
+
salts and sodium` — so the string is stored whole; a boolean `arable` column would be this layer's
|
|
160
|
+
invention. And 7 CFR 657.5 defines prime and unique farmland nationally while §657.5(c) and (d) hand
|
|
161
|
+
statewide and local importance to state and local agencies, so `Farmland of statewide importance` in Iowa
|
|
162
|
+
and in Georgia are not the same claim. `soil_map_unit.farmland_scope` carries that distinction into the
|
|
163
|
+
artifact.
|
|
164
|
+
|
|
165
|
+
## Building
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# The smoke rung: one real survey area, end to end.
|
|
169
|
+
mailwoman gazetteer build soil --area IA153 --verify
|
|
170
|
+
|
|
171
|
+
# The pilot: every published Iowa survey area.
|
|
172
|
+
mailwoman gazetteer build soil --region IA --verify
|
|
173
|
+
|
|
174
|
+
# The resolution measurement. Reports a table, not an artifact.
|
|
175
|
+
mailwoman gazetteer build soil --area IA153 --measure-resolutions 7,8,9,10
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The build is bounded by construction: one child process per range of a survey area's own FIDs, because
|
|
179
|
+
h3's WASM heap cannot be reset from JavaScript and reports an exhausted allocator as a successful empty
|
|
180
|
+
answer. A per-part zero-cell guard refuses that answer; the process bound is what makes the build
|
|
181
|
+
reproducible. Both live in `@mailwoman/spatial`'s `h3/polygon-cells.ts`, shared with `@mailwoman/flood`,
|
|
182
|
+
because the traps are properties of h3-js rather than of either product.
|
|
183
|
+
|
|
184
|
+
## Verification
|
|
185
|
+
|
|
186
|
+
`--verify` runs both halves. The positive half re-asks Soil Data Access which map unit covers a sample of
|
|
187
|
+
points drawn deterministically from the artifact, comparing **map unit against map unit** — comparing the
|
|
188
|
+
derived class instead would let a wrong delineation agree by accident whenever two neighbours share a
|
|
189
|
+
class. Disagreements carry the distance to the nearest **edge**, not to the nearest vertex: a point a
|
|
190
|
+
centimeter from a long edge can be meters from every vertex, and the flood layer's one near-miss read
|
|
191
|
+
1.58 m to vertices and 0.009 m to edges.
|
|
192
|
+
|
|
193
|
+
The negative half samples points in every neighboring state, two of them close to the Iowa border, and
|
|
194
|
+
requires `unknown` — no coverage row — rather than a low-capability reading. The positive half alone would
|
|
195
|
+
pass on an artifact that answered class 8 for the whole planet.
|
|
196
|
+
|
|
197
|
+
## The observation
|
|
198
|
+
|
|
199
|
+
Default OFF, and the switch is the presence of `$MAILWOMAN_DATA_ROOT/soil/soil.db` rather than a boolean.
|
|
200
|
+
The reading reaches a caller as one additive `QueryIntentMarker` with `code: "authority_designation"` and
|
|
201
|
+
`mechanism: "layer:soil_capability"` — the same code the flood layer's marker uses, under the same `layer`
|
|
202
|
+
family, with a rule of its own. The class never travels without the share it rests on. Ranking, abstention
|
|
203
|
+
and every existing result field are unchanged, and a test pins that a geocode without the layer is
|
|
204
|
+
byte-identical to one with it, minus the marker.
|
|
205
|
+
|
|
206
|
+
## Not this layer's job
|
|
207
|
+
|
|
208
|
+
- **No raster in the database.** gSSURGO and gNATSGO are the gridded derivatives at 10 m per state and
|
|
209
|
+
30 m for CONUS, in a projected CRS, distributed through a host that refuses anonymous programmatic
|
|
210
|
+
download. Should a builder reach for one, the raster rule applies: bin at build time to the same
|
|
211
|
+
per-cell class summary shape and store that, never the grid.
|
|
212
|
+
- **No Cropland Data Layer.** It is CC0 and measured, but it answers a different question — observed cover
|
|
213
|
+
in one season, not capability — its accuracy caveats are unread, and it is a raster ingest into a
|
|
214
|
+
repository with no raster tooling. Whoever does build it inherits a meaning-of-zero inversion that
|
|
215
|
+
arrives pre-built in the source's own encoding: the derived Crop Frequency Layer's value domain runs
|
|
216
|
+
`"1"` planted once in 18 years through `"18"` planted every year, then **`"255"` planted ZERO times**,
|
|
217
|
+
while **`"0"` is No Data**. A reader that takes 0 as "never planted" reads _we have no data here_ as
|
|
218
|
+
_nothing was ever grown here_ — exactly backwards. Nothing in this vocabulary uses a numeric sentinel
|
|
219
|
+
for either state, and nothing in it should start.
|
|
220
|
+
- **No suitability score.** The layer repeats what an authority states, in the authority's vocabulary, with
|
|
221
|
+
the authority's dates. The projection to a number belongs to the consumer, not to the layer.
|