Text Classification
Transformers
Joblib
ONNX
Safetensors
English
bert
masterformat
masterformat-classifier
csi-masterformat
construction
construction-technology
sequence-classification
tfidf
ensemble
specs
spec-writing
specifications
takeoff
estimating
cost-code
ufgs
public-domain
Eval Results (legacy)
text-embeddings-inference
Instructions to use constructelligence/masterformat-classifier with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use constructelligence/masterformat-classifier with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="constructelligence/masterformat-classifier")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer = AutoTokenizer.from_pretrained("constructelligence/masterformat-classifier") model = AutoModelForSequenceClassification.from_pretrained("constructelligence/masterformat-classifier", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Opt-in --bid-items mode: TF-IDF also trained on 2,842 real TxDOT bid items (item-held-out .489 -> .608)
Browse files- IMPROVEMENT-PLAN.md +59 -0
- README.md +32 -1
- ensemble/predict_ensemble.py +6 -1
- ensemble/tfidf_bid.joblib +3 -0
- metrics.json +67 -1
- txdot_crosswalk_train.json +50 -0
IMPROVEMENT-PLAN.md
CHANGED
|
@@ -124,3 +124,62 @@ full sections; add an `other/<division>` fallback and score it honestly; revisit
|
|
| 124 |
- More synthetic line-item augmentation (tested: `mf-0.3` — no line-item transfer, manual regression).
|
| 125 |
- More TF-IDF feature engineering (already saturated; gains below the CI).
|
| 126 |
- Reporting sub-CI improvements as wins on the model card.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
- More synthetic line-item augmentation (tested: `mf-0.3` — no line-item transfer, manual regression).
|
| 125 |
- More TF-IDF feature engineering (already saturated; gains below the CI).
|
| 126 |
- Reporting sub-CI improvements as wins on the model card.
|
| 127 |
+
|
| 128 |
+
---
|
| 129 |
+
|
| 130 |
+
## Progress — P0.1 acquisition started (2026-10-08)
|
| 131 |
+
|
| 132 |
+
**Tooling added (this stage).**
|
| 133 |
+
- `scripts/compare.py` — the gate the plan was missing: **paired bootstrap CI of the difference + exact
|
| 134 |
+
McNemar**, per set, with a `LIFT / DROP / no detectable change` verdict. `cloud/stats.py` had Wilson CIs
|
| 135 |
+
but no significance test, which is how sub-CI changes kept reading as wins.
|
| 136 |
+
- `scripts/build_manual_eval_v2.py` — section eval over **every** local manual, with a train/val
|
| 137 |
+
leakage guard (drops any chunk whose text appears in the corpus).
|
| 138 |
+
- `scripts/import_bid_tabulation.py` — pulls real line items from public bid tabulations.
|
| 139 |
+
|
| 140 |
+
**What the sources actually hold (the hard part).**
|
| 141 |
+
- The local AEC-Bench / ConstructBench cache has 18 documents, but **only 2 are project manuals**
|
| 142 |
+
(`spec.pdf` ×2 → Wenatchee 651 pp, Macon 807 pp); the rest are drawings, sheets and submittals. Building
|
| 143 |
+
over all 18 → **153 sections / 6,890 chunks, 0 leaked** — so the section eval is capped at ±8 pts by the
|
| 144 |
+
*source*, not by code. **≥400 sections needs more public project manuals.**
|
| 145 |
+
- No public MasterFormat-labelled dataset exists: Hugging Face dataset search returns **0** for
|
| 146 |
+
`masterformat` / `construction specifications` / `CSI division`; Kaggle has none either.
|
| 147 |
+
- Public bid tabulations are real but are labelled with **agency** codes, not CSI: `aniket0712/bid-tabulations`
|
| 148 |
+
(TxDOT, 960,791 rows) has `BID ITEM DESCRIPTION` + `SPECIFICATION CODE` (TxDOT's own numbering). Useful as
|
| 149 |
+
weak-label **training** text (P0.2) and as a **candidate pool** for labelling — not as a true-label eval.
|
| 150 |
+
|
| 151 |
+
**Produced.** `eval/line_items_v2_candidates.tsv` — **2,000 unique real line items** (deduped, cleaned) with
|
| 152 |
+
their source spec code and a blank `true_group` for a reviewer to fill. That is the automated half of P0.1's
|
| 153 |
+
"≥1,000 real line items"; the labels are the human half.
|
| 154 |
+
|
| 155 |
+
**Conclusion.** Both halves of P0.1 are blocked on **data and labelling, not modelling**: more public
|
| 156 |
+
project manuals for sections, and human labels (or a licensed source) for line items. The mf-0.5 Kaggle run
|
| 157 |
+
already confirmed the point — `val` rose (+2.3) while line items (−1.6) and sections (−1.3) fell, i.e. the
|
| 158 |
+
auxiliary coarse-to-fine loss bought nothing on the target distribution.
|
| 159 |
+
|
| 160 |
+
---
|
| 161 |
+
|
| 162 |
+
## Result 2026-10-13 — P0.2 first pass: real bid items in the TF-IDF scorer (`runs/bidtab/`, NOT published)
|
| 163 |
+
|
| 164 |
+
Data: full TxDOT bid tabulation (960k rows -> 13,239 unique item texts, `data/raw/txdot_bid_items_unique.jsonl`)
|
| 165 |
+
labelled by the eval crosswalk + 45 training-only codes (`data/txdot_crosswalk_train.json`, titles taken from
|
| 166 |
+
TxDOT's own SPECIFICATION DESCRIPTION column). After dropping every eval text and every eval text up to digits:
|
| 167 |
+
2,842 training items, 19 groups. Weight x3 chosen on a dev split of training-only codes. Script:
|
| 168 |
+
`scripts/bidtab_experiment.py`, results `runs/bidtab-exp.json`. Transformer unchanged (mf-0.2, blend w 0.25).
|
| 169 |
+
|
| 170 |
+
| eval | published ensemble | + bid items | paired |
|
| 171 |
+
|---|---:|---:|---|
|
| 172 |
+
| TxDOT, **item numbers held out** (5-fold grouped CV), n 790 | 0.489 [.454,.523] | **0.608** [.573,.641] | 104 vs 10, p < 0.001 |
|
| 173 |
+
| TxDOT capped <= 20/item, n 529 | 0.369 [.329,.411] | **0.492** [.449,.534] | 73 vs 8, p < 0.001 |
|
| 174 |
+
| hand-labelled building line items, n 194 | 0.691 | 0.701 | 3 vs 1, p 0.63 |
|
| 175 |
+
| manual chunks, n 6,890 | 0.326 | 0.329 | 65 vs 47, p 0.11 |
|
| 176 |
+
| whole sections top-1 / division, n 153 | 0.620 / 0.804 | 0.613 / 0.791 | -1 / -2 sections |
|
| 177 |
+
| UFGS val, n 11,598 | 0.5875 | 0.5836 | 61 vs 106, p < 0.001 |
|
| 178 |
+
|
| 179 |
+
TxDOT with item numbers *seen* (texts deduped) is 0.953 — memorisation of item types, not a headline number.
|
| 180 |
+
|
| 181 |
+
Gate check (§3): lift on real line items is large and significant on the agency set but not measurable on the
|
| 182 |
+
194-item building set; **section division -1.3 pts misses the "within 1 pt" gate by 2 sections** (noise-level,
|
| 183 |
+
but the gate was set in advance) and UFGS val drops 0.4 pts (significant). Decision left to the owner:
|
| 184 |
+
ship as default, ship as an opt-in "bid-item" TF-IDF, or first enlarge the section eval (P0.1) to tell a
|
| 185 |
+
2-section move from noise.
|
README.md
CHANGED
|
@@ -240,6 +240,36 @@ non-overlapping intervals. It is still well below the 70 % on hand-written build
|
|
| 240 |
bid items are out of domain for a UFGS-trained model, and these labels come from an item-number crosswalk
|
| 241 |
(reviewed, not per-item), covering civil work only.
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
## Calibration and abstention
|
| 244 |
|
| 245 |
The ensemble is already close to calibrated; a single **temperature** `T = 0.90` (fit on `val`) lowers the
|
|
@@ -342,9 +372,10 @@ manual-chunk and whole-section accuracy. Constructelligence's proprietary models
|
|
| 342 |
- `predict.py` — CLI example: batching, `--top-k`, `--divisions`, `--json`, stdin/file input, `--onnx`.
|
| 343 |
- `onnx/` — ONNX fp32 and int8 exports.
|
| 344 |
- `ensemble/` — `tfidf.joblib` (word + char n-grams, ~112 MB), `blend.json`, `predict_ensemble.py`: the
|
| 345 |
-
transformer + TF-IDF line-item ensemble.
|
| 346 |
- `abbrev.py` — bid-item abbreviation expansion used by both predictors (`--no-expand` to disable).
|
| 347 |
- `txdot_crosswalk.json` — TxDOT item number → MasterFormat group table behind the bid-item evaluation.
|
|
|
|
| 348 |
- `IMPROVEMENT-PLAN.md` — the prioritised roadmap (enlarged eval, data, domain-adaptive pretraining, distillation).
|
| 349 |
- `CITATION.cff`, `requirements.txt`.
|
| 350 |
|
|
|
|
| 240 |
bid items are out of domain for a UFGS-trained model, and these labels come from an item-number crosswalk
|
| 241 |
(reviewed, not per-item), covering civil work only.
|
| 242 |
|
| 243 |
+
### Bid-item mode (`--bid-items`, opt-in)
|
| 244 |
+
|
| 245 |
+
`ensemble/tfidf_bid.joblib` is the same TF-IDF scorer **also trained on 2,842 real TxDOT bid items**: the full
|
| 246 |
+
public bid tabulation (960k rows → 13,239 unique descriptions), labelled through the crosswalk above plus 45
|
| 247 |
+
training-only item codes (`txdot_crosswalk_train.json`, mapped from TxDOT's own specification titles). Every
|
| 248 |
+
evaluation description — and every description equal to one after removing numbers ("TREE REMOVAL (4" - 12" DIA)"
|
| 249 |
+
vs "(12" - 18" DIA)") — was removed from training. Bid items are weighted ×3, chosen on a dev split of
|
| 250 |
+
training-only codes.
|
| 251 |
+
|
| 252 |
+
```bash
|
| 253 |
+
python ensemble/predict_ensemble.py --bid-items "CL C CONC (RAIL FOUNDATION)" "SET (TY II) (24 IN) (RCP) (6: 1) (P)"
|
| 254 |
+
```
|
| 255 |
+
|
| 256 |
+
The honest number is **item-held-out**: 5-fold cross-validation grouped by TxDOT item number, so each evaluation
|
| 257 |
+
row is scored by a model that saw no bid item of its type (new item types, not paraphrases). Paired against the
|
| 258 |
+
default on the same rows:
|
| 259 |
+
|
| 260 |
+
| Ensemble on 790 TxDOT bid items | default | `--bid-items` | changed |
|
| 261 |
+
|---|---:|---:|---|
|
| 262 |
+
| top-1, item types held out | 0.489 [0.454, 0.523] | **0.608 [0.573, 0.641]** | 104 fixed, 10 broken (McNemar p < 0.001) |
|
| 263 |
+
| top-1 capped ≤ 20/item (n = 529) | 0.369 [0.329, 0.411] | **0.492 [0.449, 0.534]** | 73 fixed, 8 broken |
|
| 264 |
+
|
| 265 |
+
The shipped `tfidf_bid.joblib` has seen these item *types* (not these descriptions); on them it reaches 0.95 —
|
| 266 |
+
that is memorised item types, not a measure of quality on new agencies' items.
|
| 267 |
+
|
| 268 |
+
**Why it is opt-in.** On building text it is a wash or slightly worse: 194 hand-labelled line items 0.691 → 0.701
|
| 269 |
+
(3 vs 1 changed), manual chunks 0.326 → 0.329, whole sections 0.620 → 0.613 top-1 and 0.804 → 0.791 division
|
| 270 |
+
(2 of 153 sections), UFGS val 0.588 → 0.584. Use it for agency / DOT bid tabulations, keep the default for
|
| 271 |
+
specifications and building estimates.
|
| 272 |
+
|
| 273 |
## Calibration and abstention
|
| 274 |
|
| 275 |
The ensemble is already close to calibrated; a single **temperature** `T = 0.90` (fit on `val`) lowers the
|
|
|
|
| 372 |
- `predict.py` — CLI example: batching, `--top-k`, `--divisions`, `--json`, stdin/file input, `--onnx`.
|
| 373 |
- `onnx/` — ONNX fp32 and int8 exports.
|
| 374 |
- `ensemble/` — `tfidf.joblib` (word + char n-grams, ~112 MB), `blend.json`, `predict_ensemble.py`: the
|
| 375 |
+
transformer + TF-IDF line-item ensemble; `tfidf_bid.joblib` (~115 MB) is the opt-in `--bid-items` scorer.
|
| 376 |
- `abbrev.py` — bid-item abbreviation expansion used by both predictors (`--no-expand` to disable).
|
| 377 |
- `txdot_crosswalk.json` — TxDOT item number → MasterFormat group table behind the bid-item evaluation.
|
| 378 |
+
- `txdot_crosswalk_train.json` — 45 further TxDOT codes used only to label `--bid-items` training data.
|
| 379 |
- `IMPROVEMENT-PLAN.md` — the prioritised roadmap (enlarged eval, data, domain-adaptive pretraining, distillation).
|
| 380 |
- `CITATION.cff`, `requirements.txt`.
|
| 381 |
|
ensemble/predict_ensemble.py
CHANGED
|
@@ -10,6 +10,7 @@ manual-chunk accuracy). This script reproduces the blend. The weight is chosen o
|
|
| 10 |
python predict_ensemble.py "EPDM membrane roofing" "4000 psi concrete slab on grade"
|
| 11 |
python predict_ensemble.py --top-k 5 --divisions "12\" RCP storm drain pipe"
|
| 12 |
python predict_ensemble.py --json --file items.txt > out.json
|
|
|
|
| 13 |
|
| 14 |
# default TF-IDF/results live beside this script; point --model at the Hub or a local run
|
| 15 |
python predict_ensemble.py --model constructelligence/masterformat-classifier \
|
|
@@ -98,10 +99,14 @@ def main():
|
|
| 98 |
ap.add_argument("--file")
|
| 99 |
ap.add_argument("--json", action="store_true")
|
| 100 |
ap.add_argument("--no-expand", action="store_true", help="do not expand bid-item abbreviations first")
|
|
|
|
|
|
|
| 101 |
a = ap.parse_args()
|
|
|
|
|
|
|
| 102 |
|
| 103 |
if a.weight is None:
|
| 104 |
-
bf = Path(a.tfidf).with_name("blend.json")
|
| 105 |
a.weight = json.loads(bf.read_text())["weight"] if bf.exists() else 0.2
|
| 106 |
w = a.weight
|
| 107 |
|
|
|
|
| 10 |
python predict_ensemble.py "EPDM membrane roofing" "4000 psi concrete slab on grade"
|
| 11 |
python predict_ensemble.py --top-k 5 --divisions "12\" RCP storm drain pipe"
|
| 12 |
python predict_ensemble.py --json --file items.txt > out.json
|
| 13 |
+
python predict_ensemble.py --bid-items "CL C CONC (RAIL FOUNDATION)" "RE PM W/RET REQ TY I (W)6\"(SLD)" # agency bid tabs
|
| 14 |
|
| 15 |
# default TF-IDF/results live beside this script; point --model at the Hub or a local run
|
| 16 |
python predict_ensemble.py --model constructelligence/masterformat-classifier \
|
|
|
|
| 99 |
ap.add_argument("--file")
|
| 100 |
ap.add_argument("--json", action="store_true")
|
| 101 |
ap.add_argument("--no-expand", action="store_true", help="do not expand bid-item abbreviations first")
|
| 102 |
+
ap.add_argument("--bid-items", action="store_true",
|
| 103 |
+
help="agency bid-tab mode: use tfidf_bid.joblib (also trained on 2,842 real TxDOT bid items)")
|
| 104 |
a = ap.parse_args()
|
| 105 |
+
if a.bid_items and a.tfidf == str(HERE / "tfidf.joblib"):
|
| 106 |
+
a.tfidf = str(HERE / "tfidf_bid.joblib") # same blend weight (blend.json) as the default scorer
|
| 107 |
|
| 108 |
if a.weight is None:
|
| 109 |
+
bf = HERE / "blend.json" if a.bid_items else Path(a.tfidf).with_name("blend.json")
|
| 110 |
a.weight = json.loads(bf.read_text())["weight"] if bf.exists() else 0.2
|
| 111 |
w = a.weight
|
| 112 |
|
ensemble/tfidf_bid.joblib
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ac597727d5baf9e563c1f2d5ecc771de8f437ba65fa1fa7d9b1e25e967c5a4f6
|
| 3 |
+
size 115060552
|
metrics.json
CHANGED
|
@@ -70,5 +70,71 @@
|
|
| 70 |
"division": 0.57
|
| 71 |
}
|
| 72 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
-
}
|
|
|
|
| 70 |
"division": 0.57
|
| 71 |
}
|
| 72 |
}
|
| 73 |
+
},
|
| 74 |
+
"txdot_bid_items_bidmode": {
|
| 75 |
+
"_doc": "ensemble/tfidf_bid.joblib (opt-in --bid-items); item-held-out = 5-fold CV grouped by TxDOT item number; paired vs default ensemble; building-set regression with the shipped model",
|
| 76 |
+
"item_held_out": {
|
| 77 |
+
"n": 790,
|
| 78 |
+
"top1": 0.6076,
|
| 79 |
+
"ci": [
|
| 80 |
+
0.573,
|
| 81 |
+
0.641
|
| 82 |
+
],
|
| 83 |
+
"vs_published": {
|
| 84 |
+
"new_right_old_wrong": 104,
|
| 85 |
+
"old_right_new_wrong": 10,
|
| 86 |
+
"p": 0.0
|
| 87 |
+
}
|
| 88 |
+
},
|
| 89 |
+
"item_held_out_capped": {
|
| 90 |
+
"n": 529,
|
| 91 |
+
"top1": 0.4915,
|
| 92 |
+
"ci": [
|
| 93 |
+
0.449,
|
| 94 |
+
0.534
|
| 95 |
+
],
|
| 96 |
+
"vs_published": {
|
| 97 |
+
"new_right_old_wrong": 73,
|
| 98 |
+
"old_right_new_wrong": 8,
|
| 99 |
+
"p": 0.0
|
| 100 |
+
}
|
| 101 |
+
},
|
| 102 |
+
"default_same_rows": {
|
| 103 |
+
"n": 790,
|
| 104 |
+
"top1": 0.4886,
|
| 105 |
+
"ci": [
|
| 106 |
+
0.454,
|
| 107 |
+
0.523
|
| 108 |
+
]
|
| 109 |
+
},
|
| 110 |
+
"items_seen_optimistic": 0.9532,
|
| 111 |
+
"building_regression": {
|
| 112 |
+
"val": {
|
| 113 |
+
"default": 0.5875,
|
| 114 |
+
"bid_mode": 0.5836
|
| 115 |
+
},
|
| 116 |
+
"line_items": {
|
| 117 |
+
"default": 0.6907,
|
| 118 |
+
"bid_mode": 0.701
|
| 119 |
+
},
|
| 120 |
+
"manuals": {
|
| 121 |
+
"default": 0.3261,
|
| 122 |
+
"bid_mode": 0.3287
|
| 123 |
+
},
|
| 124 |
+
"manual_sections": {
|
| 125 |
+
"default": {
|
| 126 |
+
"sections": 153,
|
| 127 |
+
"top1": 0.62,
|
| 128 |
+
"division": 0.803921568627451
|
| 129 |
+
},
|
| 130 |
+
"bid_mode": {
|
| 131 |
+
"sections": 153,
|
| 132 |
+
"top1": 0.6133333333333333,
|
| 133 |
+
"division": 0.7908496732026143
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
},
|
| 137 |
+
"train_items": 2842,
|
| 138 |
+
"bid_weight": 3.0
|
| 139 |
}
|
| 140 |
+
}
|
txdot_crosswalk_train.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_doc": "TRAINING-ONLY additions to eval/txdot_crosswalk.json: TxDOT specification codes -> MasterFormat level-2 group, for labelling bid items from the full TxDOT bid tabulation (Kaggle aniket0712/bid-tabulations). Titles are TxDOT's own SPECIFICATION DESCRIPTION strings from that file, not recalled from memory. Same rules as the eval crosswalk: only titles that map to one of the 171 groups without doubt; maintenance (7xx), temporary work-zone, signs and signals (34 40 00 is not a class), mixed scopes (400 structure excavation, 423 retaining walls, 450 railing, 434 bearings, 476 jacking/boring, 533 rumble strips, 305 RAP, 401 flowable fill) and all 4-digit special specifications are left out. Grouping follows the eval crosswalk's own choices (erosion control -> 31 20 00 as item 506; removal of existing work -> 02 40 00 as items 104/496; vehicle barriers -> 34 70 00 as item 540). The eval file's 790 rows keep their original labels. Built 2026-10-13.",
|
| 3 |
+
"items": {
|
| 4 |
+
"112": ["SUBGRADE WIDENING", "31 20 00"],
|
| 5 |
+
"134": ["BACKFILLING PAVEMENT EDGES", "31 20 00"],
|
| 6 |
+
"158": ["SPECIALIZED EXCAVATION WORK", "31 20 00"],
|
| 7 |
+
"169": ["SOIL RETENTION BLANKETS", "31 20 00"],
|
| 8 |
+
"160": ["TOPSOIL", "32 90 00"],
|
| 9 |
+
"161": ["COMPOST", "32 90 00"],
|
| 10 |
+
"162": ["SODDING FOR EROSION CONTROL", "32 90 00"],
|
| 11 |
+
"166": ["FERTILIZER", "32 90 00"],
|
| 12 |
+
"180": ["WILDFLOWER SEEDING", "32 90 00"],
|
| 13 |
+
"192": ["LANDSCAPE PLANTING", "32 90 00"],
|
| 14 |
+
"170": ["IRRIGATION SYSTEM", "32 80 00"],
|
| 15 |
+
"251": ["REWORKING BASE COURSES", "32 10 00"],
|
| 16 |
+
"276": ["CEMENT TREATMENT (PLANT-MIXED)", "31 30 00"],
|
| 17 |
+
"310": ["PRIME COAT", "32 10 00"],
|
| 18 |
+
"315": ["FOG SEAL", "32 10 00"],
|
| 19 |
+
"334": ["HOT-MIX COLD-LAID ASPHALT CONCRETE PAVEMENT", "32 10 00"],
|
| 20 |
+
"340": ["DENSE-GRADED HOT-MIX ASPHALT (SMALL QUANTITY)", "32 10 00"],
|
| 21 |
+
"341": ["DENSE-GRADED HOT-MIX ASPHALT", "32 10 00"],
|
| 22 |
+
"342": ["PERMEABLE FRICTION COURSE", "32 10 00"],
|
| 23 |
+
"344": ["SUPERPAVE MIXTURES", "32 10 00"],
|
| 24 |
+
"346": ["STONE-MATRIX ASPHALT", "32 10 00"],
|
| 25 |
+
"347": ["THIN OVERLAY MIXTURES (TOM)", "32 10 00"],
|
| 26 |
+
"348": ["THIN BONDED FRICTION COURSES", "32 10 00"],
|
| 27 |
+
"407": ["STEEL PILING", "31 60 00"],
|
| 28 |
+
"409": ["PRESTRESSED CONCRETE PILING", "31 60 00"],
|
| 29 |
+
"427": ["SURFACE FINISHES FOR CONCRETE", "03 30 00"],
|
| 30 |
+
"442": ["METAL FOR STRUCTURES", "05 10 00"],
|
| 31 |
+
"459": ["GABIONS AND GABION MATTRESSES", "31 30 00"],
|
| 32 |
+
"460": ["CORRUGATED METAL PIPE", "33 40 00"],
|
| 33 |
+
"467": ["SAFETY END TREATMENT", "33 40 00"],
|
| 34 |
+
"468": ["THERMOPLASTIC PIPE CULVERTS AND DRAINS", "33 40 00"],
|
| 35 |
+
"471": ["FRAMES, GRATES, RINGS AND COVERS", "33 40 00"],
|
| 36 |
+
"474": ["LINEAR DRAINS", "33 40 00"],
|
| 37 |
+
"481": ["PIPE OR DRAINS", "33 40 00"],
|
| 38 |
+
"556": ["PIPE UNDERDRAINS", "33 40 00"],
|
| 39 |
+
"514": ["PERMANENT CONCRETE TRAFFIC BARRIER", "34 70 00"],
|
| 40 |
+
"543": ["CABLE BARRIER SYSTEM", "34 70 00"],
|
| 41 |
+
"542": ["REMOVING METAL BEAM GUARD FENCE", "02 40 00"],
|
| 42 |
+
"528": ["COLORED TEXTURED CONCRETE AND LANDSCAPE PAVERS", "32 10 00"],
|
| 43 |
+
"536": ["CONCRETE MEDIANS AND DIRECTIONAL ISLANDS", "32 10 00"],
|
| 44 |
+
"552": ["WIRE FENCE", "32 30 00"],
|
| 45 |
+
"613": ["HIGH MAST ILLUMINATION POLES", "26 50 00"],
|
| 46 |
+
"614": ["HIGH MAST ILLUMINATION ASSEMBLIES", "26 50 00"],
|
| 47 |
+
"621": ["TRAY CABLE", "26 00 00"],
|
| 48 |
+
"628": ["ELECTRICAL SERVICES", "26 00 00"]
|
| 49 |
+
}
|
| 50 |
+
}
|