The document model
Every document feature in the Rules block — field extraction, tables, checkboxes, snippets, fuzzy matching, the rule-callable document functions — reads one spatial model: a stack of rectangles carrying text. This page is the type reference for that stack. Get it right and the rest of the document pages are mechanical.
Two facts govern everything below.
- Coordinates are raw OCR units with a top-left origin. Nothing is normalised to
0..1, nothing is rescaled per page. A 300-DPI letter scan is 2550 × 3300 units. - Nothing is populated until analysis runs. Loading a document copies the pages and nothing else; every query answers empty — not an error — until analysis has filled the rest in. Both surfaces analyse for you, once, before the first rule is evaluated.
The four layers
Section titled “The four layers”| Layer | Types | Produced by | Holds |
|---|---|---|---|
| Geometry | Box, Point | a rule, or any layer above | pure rectangles, no text |
| OCR | Word, Line, Paragraph, Page, Document | ingest (hOCR, JSON, converters) | what the OCR engine emitted, unchanged |
| Text reconstruction | Block, Line, DocStats | Analyze() | words glued back into blocks and lines |
| Analysis | Document | Analyze() | blocks, lines, tables, fields, checkboxes, layouts, fonts, spatial index |
Each layer is derived from the one below it. There is no glyph or character level: Word is
the atom.
Geometry
Section titled “Geometry”Box and Point
Section titled “Box and Point”type Box struct { X0 float64 `json:"x0"` // top-left X Y0 float64 `json:"y0"` // top-left Y X1 float64 `json:"x1"` // bottom-right X Y1 float64 `json:"y1"` // bottom-right Y}
type Point struct{ X, Y float64 }A rule names a rectangle through the bbox.Box and bbox.GeoBox factories, which take the four
corners as float64. The rule language has no struct literals, so those factories are the only way
to construct one — see Documents in rules.
Every word, block, line, table, region and extraction result carries exactly one of these. The box has no type tag, no text and no page number; those live on the types that embed it.
Box methods
Section titled “Box methods”| Group | Methods |
|---|---|
| Size | Width(), Height(), Area() |
| Centres | Center() Point, CenterX(), CenterY(), Baseline() |
| Corners | TopLeft(), TopRight(), BottomLeft(), BottomRight() |
| Edges | Left(), Right(), Top(), Bottom() |
| Containment | Contains(Point), ContainsBox(Box), Intersects(Box) |
| Combination | Intersection(Box), Union(Box), Expand(margin) |
| Validity | IsZero(), IsValid() |
Package-level: UnionAll([]Box) Box, Distance(Point, Point) float64,
BoxDistance(Box, Box) float64.
Spatial predicates
Section titled “Spatial predicates”| Function | Returns |
|---|---|
HorizontalOverlap(a, b) / VerticalOverlap(a, b) | overlap extent in units |
HorizontalGap(a, b) / VerticalGap(a, b) | gap in units (0 when overlapping) |
IsHorizontallyAligned(a, b, tolerancePct) | same visual row |
IsVerticallyAligned(a, b, AlignType, tolerancePct) | shared edge or centre |
IsSameLine(a, b, baselineTolerance, maxGapMultiplier, medianCharWidth) | same line and close enough |
RelativePosition(a, b) Direction | dominant direction of b from a |
IsDirectlyRight / IsDirectlyLeft / IsDirectlyBelow / IsDirectlyAbove | direction with alignment |
OverlapRatio(a, b), IoU(a, b) | 0..1 overlap fractions |
AlignType is one of AlignLeft, AlignRight, AlignCenterH, AlignTop, AlignBottom,
AlignCenterV, AlignBaseline.
IsHorizontallyAligned is the workhorse: tolerance is a fraction of the smaller box height,
and the test passes on either a baseline match or a vertical-centre match. Word merging, line
formation and same-row detection all funnel through it.
The coordinate system
Section titled “The coordinate system”| Property | Value |
|---|---|
| Origin | top-left of the page |
| Y direction | increases downward |
| Units | whatever the producer emitted — in practice image pixels at scan resolution |
| Normalisation | none |
| Page numbering | 1-indexed, assigned in document order |
Because units are resolution-dependent, a hard-coded pixel threshold in a rule breaks the moment the scan resolution changes. Two mechanisms exist to avoid that:
- Percentage regions.
InRegionPct(x0Pct, y0Pct, x1Pct, y1Pct)takes0..100and multiplies byStats.PageWidth/Stats.PageHeight.doc.InRegionPct(0, 0, 40, 8)is the top-left 40% wide, top 8% tall band.InRegion(x0, y0, x1, y1)takes raw units. - Median-relative thresholds. Every internal threshold is a multiple of a
DocStatsmedian (character width, line height, word gap, line gap) rather than an absolute count.
The OCR layer
Section titled “The OCR layer”A faithful record of the producer’s output. These are the types the on-disk document JSON serialises to, and the types every ingest path produces.
type Word struct { Text string `json:"text"` Box geometry.Box `json:"box"` Confidence float64 `json:"confidence"` // normalised to 0..1 Page int `json:"page"` LineID int `json:"line_id,omitempty"`}
type Line struct { ID int `json:"id"` Text string `json:"text"` Box geometry.Box `json:"box"` Confidence float64 `json:"confidence,omitempty"` WordStart int `json:"word_start"` // half-open range into Page.Words WordEnd int `json:"word_end"`}
type Paragraph struct { ID int `json:"id"` Text string `json:"text"` Box geometry.Box `json:"box"` Confidence float64 `json:"confidence,omitempty"` LineStart int `json:"line_start"` // half-open range into Page.Lines LineEnd int `json:"line_end"`}
type Page struct { Number int `json:"number"` Width float64 `json:"width"` Height float64 `json:"height"` Words []Word `json:"words"` Lines []Line `json:"lines,omitempty"` Paragraphs []Paragraph `json:"paragraphs,omitempty"`}
type Document struct { Pages []Page `json:"pages"`}Line.Text and Paragraph.Text are the joined member text (words by space, lines by newline),
the box is the union of member boxes, and confidence is the arithmetic mean of member
confidences. NewWord(text, x0, y0, x1, y1, confidence, page) constructs a word;
Word.EstimateCharWidth() returns Box.Width() / len(Text).
Document methods at this layer: GetPage(pageNum int) []Word, AllWords() []Word,
PageCount() int, UnescapeText(), CleanDocument(), CleanDocumentWithOptions(opts).
The on-disk document JSON
Section titled “The on-disk document JSON”{ "pages": [ { "number": 1, "width": 2550, "height": 3300, "words": [ { "text": "HEADER", "box": { "x0": 100, "y0": 100, "x1": 400, "y1": 160 }, "confidence": 0.95, "page": 1, "line_id": 1 } ], "lines": [ { "id": 1, "text": "HEADER", "box": { "x0": 100, "y0": 100, "x1": 400, "y1": 160 }, "confidence": 0.95, "word_start": 0, "word_end": 1 } ] } ]}Field names are exactly the struct JSON tags. lines and paragraphs are omitted when empty.
This is the shape written to .bbox files, read back by every rule-execution command, and sent
inline as document input on POST /rule.
The text-reconstruction layer
Section titled “The text-reconstruction layer”Analyze() rebuilds words into blocks and blocks into lines.
type Block struct { Text string Box geometry.Box Words []ocr.Word // the source words, retained for provenance Confidence float64 Page int LineID int // 0 when the producer supplied no line grouping IsMerged bool MergeScore confidence.Score // confidence that the merge was correct}
type Line struct { Blocks []Block Box geometry.Box CachedText string Page int IsWrapped bool Confidence float64 FormationScore confidence.Score}| Type | Methods |
|---|---|
Block | GetPage(), FontSizePixels() (box height), FontSizePoints(dpi), CharWidthPixels() |
Line | GetPage(), FontSizePixels() (median of member blocks), FontSizePoints(dpi), Text(), AverageConfidence() |
FontSizePoints converts as height * 72 / dpi, divided by a fixed 0.85 ascender ratio. A
non-positive DPI falls back to 300.
Word merging
Section titled “Word merging”MergeWords sorts words into reading order, then walks adjacent pairs. Two words join the same
block when they are horizontally aligned and the horizontal gap is below
MedianCharWidth * GapThresholdMultiplier (default 0.5; a 5.0-unit fallback applies when the
statistic is unusable). With pattern merging enabled, a gap up to three times the threshold
still merges when the concatenation looks like a partial pattern — a split social-security or
phone number, for example.
This is why block text can differ from the raw OCR tokens, and why Block.Words is retained.
The word sort is deliberately band-anchored: words are ordered by centre Y, then partitioned into line bands by testing each word against the band’s first word only, then sorted by band and left edge. A naive “same line → by X, else by Y” comparator is not a strict weak ordering (line alignment is not transitive on skewed scans) and produced different block segmentation run to run for the same page.
Line formation
Section titled “Line formation”If any block carries a non-zero LineID, blocks are grouped by (page, LineID) and the
resulting lines sorted by page then centre Y. Otherwise lines are formed geometrically: sort by
centre Y, then chain blocks into a line while each is horizontally aligned with the previous
block at 0.5 tolerance. Members are then sorted left-to-right, boxes unioned, text joined with
spaces, and a formation confidence computed from OCR confidence, vertical-alignment spread and
gap variance.
IsWrapped marks a visual continuation of the previous line. It is set when the line is
indented past twice the median character width or the vertical gap is under 0.7 of the
median line gap, and the line does not look like a form label.
Document statistics
Section titled “Document statistics”type DocStats struct { MedianCharWidth float64 MedianLineHeight float64 MedianWordGap float64 MedianLineGap float64 PageWidth float64 PageHeight float64 PageDimsDerived bool}The four medians are computed from the words alone. Page dimensions are seeded from the page declaration.
Derived page dimensions
Section titled “Derived page dimensions”When a page declares no width and height, they are derived from the words’ bounding extent and
PageDimsDerived is set to true.
{ "pages": [ { "number": 1, "words": [ { "text": "HEADER", "box": { "x0": 100, "y0": 100, "x1": 400, "y1": 160 }, "page": 1 }, { "text": "body", "box": { "x0": 100, "y0": 1600, "x1": 300, "y1": 1660 }, "page": 1 }, { "text": "corner", "box": { "x0": 2000, "y0": 3000, "x1": 2400, "y1": 3060 }, "page": 1 } ] } ]}That page declares no width and no height, so analysis derives them: Stats.PageWidth is
2400 (the largest x1 of any word), Stats.PageHeight is 3060 (the largest y1), and
Stats.PageDimsDerived is true.
Derived dimensions under-estimate the physical page — the margins past the last word are lost —
so percentage regions are slightly over-scaled. That is a bounded approximation instead of every
region query matching nothing. Inputs that reach this state: document JSON that omits width and
height, hOCR without a page bbox, and malformed page titles.
The analysed document
Section titled “The analysed document”type Document struct { Pages []ocr.Page // raw input
Stats text.DocStats // computed statistics Blocks []text.Block // reconstructed text Lines []text.Line
Tables []table.Table Fields []field.Field Checkboxes []checkbox.Checkbox CheckboxGroups []checkbox.CheckboxGroup PageLayouts []*region.PageLayout
SpecialRegions []special.Region Signatures []special.Region Handwriting []special.Region Barcodes []special.Region
FontAnalysis *font.FontAnalysis // unexported: spatial index, font estimator, reading cache, config, schemas}Loading a document fills Pages and nothing else. Every other field is populated by analysis, which
runs once, before the first rule is evaluated.
The analysis pipeline
Section titled “The analysis pipeline”Analyze() resets all derived state, then runs a fixed order. Later stages consume earlier ones.
per page: ComputeStats → the four medians from this page's words SeedPageDims → page width/height, with extent fallback MergeWords → words into blocks FormLines → blocks into lines DetectWrappedLines → IsWrapped flags
document-wide: aggregate statistics build the spatial index over all blocks detect tables → Tables detect fields → Fields detect checkboxes → Checkboxes, CheckboxGroups detect page regions → PageLayouts analyse fonts → FontAnalysis detect special regions → SpecialRegions, Signatures, Handwriting, BarcodesAnalysis settings
Section titled “Analysis settings”Analysis is not configurable — neither the CLI nor rules.svc exposes a flag, a bootstrap key or a
request field for any of it. Every document is analysed with these fixed values. They are listed
because they explain the results you get, not because you can change them.
| Setting | Fixed value | Effect |
|---|---|---|
| Word merging | on | Adjacent words are glued into blocks. |
| Table detection | on | Populates Tables. |
| Field detection | on | Populates Fields. |
| Checkbox detection | on | Populates Checkboxes and CheckboxGroups. |
| Region detection | on | Populates PageLayouts (header, footer, main content, margins, column count). |
| Special-region detection | on | Populates SpecialRegions and its projections. |
| Font analysis | on | Calibrates the font-size estimator; populates FontAnalysis. |
| Spatial index | on | Builds the R-tree over all blocks that the nearest-block queries read. |
| DPI | 300 | Scan resolution for point-size conversion. |
| Merge gap threshold | 0.5 | Merge gap as a multiple of MedianCharWidth. |
| Pattern merging | on | Allows a merge up to 3× the threshold for partial patterns. |
The fixed DPI is what the pipeline’s own point-size conversion uses. FontSizePoints(dpi) takes its
own value and falls back to 300 when that value is non-positive, so pass the resolution your scans
were produced at: 72 (PDF), 96 (screen), 150 (low-resolution scan), 300 (high-resolution scan).
Reading order
Section titled “Reading order”Reading order is computed lazily on first use, cached on the document, and invalidated by
Analyze(). It classifies every block, detects columns, and then sorts.
Region classification
Section titled “Region classification”| Region | Rule |
|---|---|
RegionHeader | block centre above 12% of page height |
RegionFooter | block centre below 90% of page height |
RegionLeftMargin | block ends inside the left 10% |
RegionRightMargin | block starts past the right 10% |
RegionBody | everything else |
Content classification
Section titled “Content classification”The ratio is the block height divided by the document’s median line height. There is no bold detection, no font-name signal and no indentation signal — heading hierarchy comes from box height alone.
| Condition | Result |
|---|---|
| ratio ≥ 1.5 and centre in the top 35% of the page | ContentTitle, level 1 |
| ratio ≥ 1.5 elsewhere | ContentHeading, level 1 |
| ratio ≥ 1.2 | ContentHeading, level 2 |
starts with -, a bullet, * or a digit, under 200 characters | ContentListItem |
| ratio < 0.8, below the footer boundary | ContentFootnote |
| ratio < 0.8, elsewhere | ContentCaption |
| anything else | ContentBody |
Blocks under 3 characters, or less than 50% alphanumeric, are forced to ContentBody.
Column detection
Section titled “Column detection”Collect every block’s X0 and X1, sort them, and look for gaps of at least three times the median
character width. A gap qualifies only when fewer than 10% of blocks straddle its midpoint. The
largest qualifying gaps — at most MaxColumns - 1, with MaxColumns defaulting to 4 — become
column boundaries, and each block takes the column whose span contains its centre X.
That 10% straddle tolerance is why a two-column page with one full-width banner still splits correctly, and why a page with a wide table often reports a single column.
The sort
Section titled “The sort”ReadingBlock embeds text.Block and adds ContentType, ReadingOrder (0-indexed position in
the final sequence), Level (1 = H1, 2 = H2, 0 = body), Column (0-indexed left to right, -1
for single column) and RegionType.
The comparator is a stable sort on, in order:
- page number ascending;
- region priority — header (0) < body (1) < left/right margin (2) < footer (3);
- inside header and footer only, content type ascending (title < heading < body);
- column index ascending, when more than one column was detected;
- centre Y ascending, when the two boxes are not in the same vertical band;
- centre X ascending otherwise.
Two boxes are in the same vertical band when their estimated baselines are within 30% of the smaller box height. This is a comparator, not a layout engine: there is no XY-cut, no whitespace analysis and no Z-order.
The practical consequence is that any multi-line captured value joins row by row, not by left edge:
// label "Address" at (100,100)-(240,130), then a two-line value:// line 1 indented at x=200: "123" "Main" "St" (y 160-190)// line 2 starting further LEFT at x=100: "Apt" "4B" (y 200-230)// capturing below the label yields:"123 Main St Apt 4B"// a naive sort by left edge would have produced "Apt 4B 123 Main St"Title, outline and sections
Section titled “Title, outline and sections”Title candidates are blocks on the lowest-numbered page whose centre is in the top 35% of the page and which pass the noise filter. Each is scored on normalised height (0.40), a content score (0.30), normalised width (0.10), word count (0.10), character count (0.05) and position (0.05). The winner is then extended left-to-right with every block on the same visual line that is at least 60% as tall, and those texts are joined with spaces.
OutlineEntry{Text, Level, Box, Page, Children, ContentStart, ContentEnd} is built by walking
blocks in reading order and pushing or popping a level stack on every title or heading.
Section{Heading, Level, Blocks, Subsections, Box, Page} is built in the same pass; content
appearing before the first heading lands in an untitled level-0 section.
Section.Text() and Section.TextWithOptions(includeHeading, lineSeparator) flatten a section
and all of its subsections.
The whole analysis is held in one ReadingResult{Blocks, Outline, Sections, Columns, Title, PageCount}; the document API projects from it.
The document API
Section titled “The document API”Getting a document loaded
Section titled “Getting a document loaded”You never load or analyse a document by hand. You hand one to a rule execution, and it is read,
wrapped and analysed before the first rule is evaluated. Both surfaces do it, and both bind the
result under a name a rule passes to bbox.Get.
From the CLI, the document is a file beside the facts:
kis ocr page -r classify.grl -i pages/ page1kis rules -r classify.grl -i pages/ -b invoice=pages/page1.bboxFrom rules.svc, the document travels on the same execute-rule request as the facts, under the
reserved bbox key — document name to document JSON:
{ "name": "invoice-checks", "currency": "EUR", "bbox": { "invoice": { "pages": [ { "number": 1, "width": 2550, "height": 3300, "words": [ { "text": "HEADER", "box": { "x0": 100, "y0": 100, "x1": 400, "y1": 160 }, "page": 1 }, { "text": "body", "box": { "x0": 100, "y0": 1600, "x1": 300, "y1": 1660 }, "page": 1 } ] }, { "number": 2, "width": 2550, "height": 3300, "words": [ { "text": "SECOND", "box": { "x0": 100, "y0": 100, "x1": 400, "y1": 160 }, "page": 2 } ] } ] } }}name selects the rule set and bbox carries the documents; every other top-level key is a fact.
The inner keys of bbox are the document names, so this payload answers bbox.Get("invoice") —
the same name -b invoice=path produces on the CLI. Analysis, the bindings and the failure
behaviour are identical on both surfaces; the request headers, status codes and error envelope are
on the service page.
Whichever surface you use, set page on every word, not just the page’s number — page filtering
reads the word’s own field. With the two-page document above,
InRegionPct(0, 0, 100, 10) returns the top 10% band (HEADER, not body) and
InRegionPct(0, 0, 100, 100) returns the whole page.
Public wrapper types
Section titled “Public wrapper types”The wrapper layer is the stable, serialisable surface: flat structs with no internal package types, safe to hand to rules.
| Type | Fields |
|---|---|
BoundingBox | X0, Y0, X1, Y1 plus Width/Height/Area/CenterX/CenterY/Left/Right/Top/Bottom/IsZero/Contains(x,y) |
Point | X, Y |
TextBlock | Text, Box, Confidence, Page |
TextLine | Text, Box, Confidence, Page, IsWrapped |
Confidence | Value, Level plus IsHigh/IsMedium/IsLow/Percent |
DocumentStats | the six DocStats numbers |
Direction | DirRight, DirLeft, DirAbove, DirBelow |
PageLayoutResult | Page, Width, Height, ColumnCount, HasHeader, HasFooter, Margins |
MarginsResult | Top, Bottom, Left, Right |
RegionResult | Type, Box, Page, Text, Confidence |
Walking the document
Section titled “Walking the document”| Group | Methods |
|---|---|
| Blocks and lines | GetBlocks(), GetLines(), GetPageBlocks(page), GetPageTextLines(page) |
| Text search | FindTextBlocks(s), FindAnchorBlocks(s), FindFuzzyBlocks(s, maxDist) |
| Spatial | BlocksInBox(box), BlocksIntersectingBox(box), NearestBlocks(point, k), NearestBlockIn(from, dir, maxDist, margin), BlocksNearPoint(center, radius) |
| Layout | GetLayout(page), GetHeaderRegion(page), GetFooterRegion(page), GetMainContentRegion(page) |
| Typography | GetHeadingTextBlocks(), GetBlocksByFontSizeRange(minPt, maxPt) |
| Statistics | GetDocumentStats() |
| Checkboxes | GetCheckbox(label), GetAllCheckboxes(), GetCheckboxGroupResult(name), GetAllCheckboxGroups() |
| Special regions | GetSignatures(page), GetAllSignatures(), GetAllHandwriting(), GetAllBarcodes(), GetAllSpecialRegions() |
Internal-typed accessors return the analysis types instead of the wrappers:
GetPage(page) []text.Block, GetPageLines(page) []text.Line, GetField(label),
GetTable(containing), GetPageLayout(page), GetHeader/GetFooter/GetMainContent(page),
GetCheckedValue(groupName), Summary().
Directional and region queries
Section titled “Directional and region queries”After(anchor), Below(anchor), Above(anchor), RightOf(anchor), LeftOf(anchor),
Near(anchor), QueryBetween(start, end), InRegion(...) and InRegionPct(...) all return a
chainable query result. After tries right-of first, then below. This is the anchor-relative
half of the model: find a known label, then read what sits in a given direction from it. See
Extracting fields.
Reading-order API
Section titled “Reading-order API”| Group | Methods |
|---|---|
| Text | ReadAll(), ReadPage(page), MainContent(), BodyText(), TextUnder(heading), TextBetween(start, end) |
| Containment | Contains(s), ContainsAll(...), ContainsAny(...), InHeader(s), InFooter(s) |
| Title | Title(), HasTitle(s), TitleContains(s), TitleMatches(regex) |
| Structure | Outline(), Headings(level), AllHeadings(), Sections(), Section(heading) |
| Columns | Columns(), Column(index), ColumnCount(), IsMultiColumn() |
| Regions | HeaderText(page), FooterText(page) |
| Shape | PageCount(), IsMultiPage() |
| Cache | InvalidateReadingCache() |
Page 0 means “all pages” for HeaderText and FooterText. Headings(0) returns every level.
Section(name) falls back to a case-insensitive substring match and recurses into subsections.
The int64/float64 contract
Section titled “The int64/float64 contract”Public method parameters and return values on the analysis layer use int64 and float64
exclusively — never int, int32, uint or float32. The rule engine dispatches by
reflection and cannot handle other numeric widths. That is why PageCount() int64,
Headings(level int64) and NearestBlocks(point, k int64) look over-specified, and why a rule
must match every numeric literal to the parameter type — a whole number is int64, a decimal is
float64, and nothing converts between them.
Input formats
Section titled “Input formats”| Format | Accepted as | Notes |
|---|---|---|
Document JSON (.bbox, .json) | file path, or inline JSON on the request | The native format both surfaces consume |
hOCR (.hocr, .html, .htm) | file path | Parsed on load; kis hocr2bbox converts it to .bbox up front |
| PaddleOCR, Tesseract, Docling output | — | No parser reads these. Emit document JSON from your OCR step |
Detection is by extension and, for HTML, by content: .json parses as JSON; .hocr parses as hOCR;
.html and .htm are sniffed for ocr_page, ocrx_word or ocr_word and parsed as hOCR, else
tried as JSON; anything else — .bbox included — is tried as JSON and then as hOCR. Inline document
input on POST /rule is already parsed JSON and skips detection entirely.
Text cleanup happens at conversion time, not at load time: kis hocr2bbox --clean normalises
unicode, quotes, dashes and ligatures and strips control characters, and --clean-form additionally
normalises checkbox and radio indicators. See the flag table below.
hOCR element classes
Section titled “hOCR element classes”| Level | Recognised classes |
|---|---|
| Page | ocr_page |
| Content area | ocr_carea, ocr_photo, ocr_graphic, ocr_separator, ocr_noise |
| Block | ocr_par, ocr_textfloat, ocr_caption, ocr_header, ocr_footer |
| Line | ocr_line, ocrx_line, ocr_textline, ocr_header_line |
| Word | ocrx_word, ocr_word |
| Character | ocrx_cinfo, ocr_cinfo |
Ingest is a real HTML parse, not a regular expression. Geometry comes from the bbox property
of the title attribute; confidence from x_wconf (0–100, divided by 100) or x_conf (0–1, or
divided by 100 when greater than 1). Three passes run on every page — paragraphs first
(consuming their line and word subtrees), then bare lines outside any block, then bare words
outside both — so a page that mixes structure levels loses nothing. Elements with no bbox
inherit the nearest ancestor’s box.
<html><body><div class="ocr_page" id="page_1" title='image "page_1.png"; bbox 0 0 2550 3300; ppageno 0'> <p class="ocr_par" title="bbox 100 100 400 160"> <span class="ocr_line" title="bbox 100 100 400 160"> <span class="ocrx_word" title="bbox 100 100 400 160; x_wconf 95">HEADER</span> </span> </p></div></body></html>Page dimensions come from the width and height of the page bbox — 2550 × 3300 is US Letter at
300 DPI. x_wconf 95 becomes confidence 0.95.
The title attribute is split on ; honouring double-quoted values (an image path may contain
a semicolon), and bbox must match as a whole token followed by whitespace. A malformed bbox
yields no box rather than scavenging digits from elsewhere in the title:
bbox 100 100 200 200 -> {100,100,200,200}image "/tmp/scan.png"; bbox 0 0 2550 3300; ppageno 0 -> {0,0,2550,3300}x_bboxes 10 5 10 10 20 5 30 10; bbox 100 100 200 200 -> {100,100,200,200}image "a;bbox 9 9 9 9.png"; bbox 1 2 3 4 -> {1,2,3,4}image "/tmp/page.png"; ppageno 0 -> no boxbboxes 1 2 3 4 -> no boxbbox a b c d; ppageno 7 -> no boxLossy ingest is silent by default. Warnings are collected only in verbose mode, and the non-verbose loaders discard the parse result. See OCR ingest for the warning types and the verbose validation pass.
Converting and merging from the CLI
Section titled “Converting and merging from the CLI”kis hocr2bbox -i page1.hocrkis hocr2bbox -i page1.hocr -o page1.bbox -vkis hocr2bbox -i ./hocr -o ./bbox -w 8 --clean-form| Flag | Default | Meaning |
|---|---|---|
--input, -i | — | source hOCR file, or a directory (globs *.hocr) |
--output, -o | input path with the extension replaced by .bbox | destination file or directory |
--workers, -w | 4 | parallel workers in directory mode |
--verbose, -v | false | log every parse warning plus a missed-content summary |
--clean | false | normalise unicode, quotes, dashes, ligatures; strip garbage and control characters; fix common misreads |
--clean-form | false | --clean plus normalising checkbox and radio indicators to ASCII [ ] [x] ( ) (x); wins over --clean |
Multi-file loads concatenate all pages into one document and renumber pages sequentially from 1, updating each word’s page field. A page number is therefore a position in the merge order, never a source-document page identifier.
Reaching the model from a rule
Section titled “Reaching the model from a rule”A rule reaches documents through a named registry: every document supplied with the execution — by
the CLI from a file, by rules.svc from the request — is loaded and analysed before the rules run,
and rules fetch them by name. Because a rule cannot construct a struct, the document bindings supply
value factories for boxes, points and directions. The full surface, the naming rules and the
per-execution isolation guarantees are on
Documents in rules.
Continue with
Section titled “Continue with”- Extracting fields — anchors, directional capture, forms and checkboxes
- Tables — table detection, columns and master-detail
- OCR ingest — producers, cleanup and parse warnings
- Matching and confidence — fuzzy matching and scoring
- Documents in rules — the rule-callable surface
- The CLI — conversion, per-page and whole-document rule execution
- Running the service — executing the same rule sets, with the same documents, over HTTP