The Document API
Rules read documents through bbox.Doc(), a curated facade designed for rule authors. It
is the API to write against.
Four things to learn
Section titled “Four things to learn”A rule reads a document with an entry, a scope, a verb, and a coercion. That is the whole shape:
bbox.Doc() .Top(10) .Has("Closing Disclosure")// entry scope verb
out.Set("amount", bbox.Doc().Right("Loan Amount").Currency());// entry verb coercionScopes narrow, verbs ask or fetch, coercions turn a hit into a typed value. Most rules need one of each.
Matching is fuzzy and case-insensitive by default
Section titled “Matching is fuzzy and case-insensitive by default”You do not configure matching. It is loose out of the box.
This is the single biggest change from the old surface, and it is easy to under-read as a convenience. It is not — it removes a category of work.
bbox.Doc().Has("closing disclosure") // matches "CLOSING DISCLOSURE"bbox.Doc().Has("SELLER'S TRANSACTION") // also matches "SELLERS TRANSACTION"Case, spacing and the apostrophe/punctuation variants that OCR produces are absorbed. You write the phrase as a human would say it, once.
The old API made this the author’s problem. Every call carried tolerance numbers:
.FindIPhraseFuzzy("Loan Terms", 2, 4, 0, 0).FuzzyTextNear("Name", "right", 1, 2, 0.3)Nobody could tell you what 2, 4, 0, 0 should be for a phrase they had not seen. So authors
copied them from a neighbouring rule, and a rule that failed to match was debugged by nudging
numbers. Worse, it forced enumeration: a rule listing both SELLER'S TRANSACTION and
SELLERS TRANSACTION exists only because the matcher would not absorb the difference.
The facade derives the tolerance from the phrase instead. A surface you choose from can be used correctly by a person and by a small language model; a surface you tune cannot. That was a design constraint, not a nicety.
Opting out
Section titled “Opting out”When you genuinely need strictness — a document code, an identifier, a legal string where a near-miss is wrong:
| Modifier | Effect |
|---|---|
Exact() | Turns off loose matching for that chain |
MatchCase() | Case-sensitive, and implies Exact() |
bbox.Doc().Exact().Has("FORM-1099-B")bbox.Doc().MatchCase().Has("SECTION A")MatchCase() implies Exact() deliberately: case-sensitive-but-still-fuzzy is not a
combination that means anything useful, so the facade does not offer it.
Reach for these when a false positive costs you something. Leave them off otherwise — the
default is right far more often than not, and every Exact() is a phrase you have promised to
keep spelled exactly that way.
bbox.Doc() // the documentbbox.Doc("page") // a specific loaded document, by nameThe name is optional because it is almost always ceremony — in the production corpus, 3,320 of 3,360 document lookups pass the same string. Supply it only when a rule genuinely works across more than one loaded document.
Scopes
Section titled “Scopes”Each returns a narrowed Doc, so they chain.
| Scope | Narrows to |
|---|---|
Top(pct) / Bottom(pct) | A band measured from the top or bottom, as a percentage |
Band(fromPct, toPct) | An arbitrary horizontal band |
Region(x0, y0, x1, y1) | An explicit rectangle, in percent |
Page(n) | One page |
Section(heading) | From a heading to the end of its section |
Between(start, end) | Between two phrases |
Until(phrase) | From here until a phrase |
Line() / Lines(n) | The current line, or the next n |
Exact() / MatchCase() | Opt out of loose matching — see Matching |
Top(10) reads better than a rectangle and is what most rules meant. There is no fixed
“header”: the corpus used depths of 15, 20, 25, 30, 40 and 50 percent, so the depth is yours to
choose.
Presence verbs
Section titled “Presence verbs”| Verb | Returns |
|---|---|
Has(phrase) | bool |
HasAll(phrases...) | bool — every phrase present |
HasAny(phrases...) | bool — at least one |
HasPattern(pattern) | bool |
Count(phrase) / CountOf(pattern) | int |
Checked(label) | bool — checkbox state |
Label-relative reads
Section titled “Label-relative reads”The four directional verbs read a value positioned relative to a label, and each returns a
Value:
bbox.Doc().Right("Loan Amount")bbox.Doc().Below("Total")bbox.Doc().Left("Balance")bbox.Doc().Above("Signature")Anchor chains
Section titled “Anchor chains”Extra arguments form a chain: each anchor is located below the one before it, and the read happens in the verb’s direction from the last.
bbox.Doc().Right("Date Issued", "Closing Date")bbox.Doc().Below("Current name", "First name")This replaced three separate string DSLs. Two were catalogued at the outset —
"Date Issued<below>|Closing Date" and the chained variants — and a third turned up on
inspection: a " |> "-separated anchor path, used at 111 sites, the fifth most-used method in
the corpus.
They all meant the same thing. The third only ever walked downward, and of the other’s 161
directional hops, 146 were <below>. Three grammars for one idea, which is why they collapse into
arguments rather than syntax.
Two things improve by making them arguments rather than syntax. Each label is separately checkable, so a failing chain reports which link broke instead of returning an empty string. And there is no string grammar to get subtly wrong.
RightOf(label) covers the rightward hops, which were 15 of the 161.
Coercions
Section titled “Coercions”A Value becomes typed by asking:
| Coercion | Produces |
|---|---|
Str() / Text() | String |
Num() / Number() | Number |
Currency() | Money, symbols and separators handled |
Date() | Date |
Percent() | Percentage |
YesNo() | Boolean from yes/no text |
TitleCase() | Title-cased string |
And for inspecting the read itself:
| Method | Use |
|---|---|
Found() / Missing() | Whether anything matched |
Or(fallback) | A default when missing |
Confidence() | The match confidence |
Match() | What actually matched |
Why() / Reason() | Why a read failed — the debugging entry point |
Why() is the one to reach for when a rule silently produces nothing. A Value that did not
match knows which anchor in its chain failed.
Tables, forms and rows
Section titled “Tables, forms and rows”| Entry | Returns | For |
|---|---|---|
Table() / TableWith(text) | Table | The document’s table, or one containing text |
Form(name) / HasForm(name) | Form | A named form |
Rows(startHeading, endHeading) | Rows | A column-aligned section between two headings |
Cell(rowLabel, colLabel) | Value | One cell by its labels |
Table offers Row, RowWith, Column, Cell, Headers, Records, RowCount,
ColumnCount. Rows — for column-aligned sections that are not real tables — offers Col,
ColNth, ColRightOf, ColUnder, SplitOn, Strip, Records, Count.
Records() on either gives you the rows as maps, which is usually what you want to hand to
out.Set.
Typography and structure
Section titled “Typography and structure”| Method | Use |
|---|---|
Title() / IsTitle(phrase) | The document title |
Headings() / IsHeading(phrase) / HeadingCount() | Heading structure |
IsBigText(phrase) | Whether text is visually prominent |
FontSize(phrase) / BodyFontSize() | Type sizes |
LineWith(phrase) / FirstLine() / LineCount() | Lines |
PageCount() / TableCount() | Counts |
AllText() | Everything, as one value |
Typography signals are how you distinguish a heading that happens to read like body text from one that is actually set as a heading.
What this replaced
Section titled “What this replaced”The same classification rule, before and after:
// Previously — six lines, twelve magic numbers, two presence idiomsbbox.Get("page").InRegionPct(0.0, 0.0, 60.0, 10.0) .ContainingIPhraseFuzzy("Closing Disclosure", 2, 4).Count() > 0 &&bbox.Get("page").FindIPhraseFuzzy("Loan Terms", 2, 4, 0, 0).Len() > 0 &&bbox.Get("page").FindIPhraseFuzzy("Projected Payments", 2, 4, 0, 0).Len() > 0 &&bbox.Get("page").FindIPhraseFuzzy("SELLER'S TRANSACTION", 2, 4, 0, 0).Len() == 0 &&bbox.Get("page").FindIPhraseFuzzy("SELLERS TRANSACTION", 2, 4, 0, 0).Len() == 0// Nowbbox.Doc().Top(10).Has("Closing Disclosure") &&bbox.Doc().HasAll("Loan Terms", "Projected Payments") &&!bbox.Doc().Has("SELLER'S TRANSACTION")The second negative check disappears entirely: loose matching absorbs the apostrophe variant, so there is nothing to enumerate.
The tuning numbers are gone too, and that is the deeper point. 2, 4, 0, 0 were fuzz
thresholds an author had to tune; the facade derives them. A surface you choose from is
one both a person and a small model can use correctly — a surface you tune is not.
Migration
Section titled “Migration”| Instead of | Write |
|---|---|
bbox.Get("page") | bbox.Doc() |
.InRegionPct(0,0,100,20) | .Top(20) |
.InRegionPct(0,0,100,100) | nothing — it was a no-op |
.ContainingIPhraseFuzzy(p, 2, 4).Count() > 0 | .Has(p) |
.FindIPhraseFuzzy(p, 2, 4, 0, 0).Len() > 0 | .Has(p) |
.FuzzyTextNear(l, "right", …) | .Right(l) |
.FuzzyTextNearCapped(l, …) | .Right(l) |
.GetOCRTextSimple("A |> B") | .Below("A", "B") |
"A<below>|B" chains | .Below("A", "B") |
.IsCheckboxChecked(l) | .Checked(l) |
.ExtractColumnAlignedSection(…) | .Rows(start, end) |
.SafeExtractForm(n) / .HasForm(n) | .Form(n) / .HasForm(n) |
Existing rules do not need rewriting to keep working. Migrate when you touch a rule for another reason.
The runtime half
Section titled “The runtime half”The facade fixes what a rule reads. The runtime fixed what a rule writes over the same
period — Retract became automatic, Changed left the rule surface, and dotted output keys
nest. Together they are why the example above is three lines rather than fourteen. See
Writing rules.
See also
Section titled “See also”- Built-in functions — the full callable surface
- Writing rules — rule structure and the output envelope
- The document model — what the spatial model contains