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