OCR and text acquisition
The Rules block ingests OCR output. It does not perform OCR.
You run a recogniser yourself, out of band. If it emits hOCR, kis hocr2bbox converts that to a
.bbox word-box file; if it already emits word boxes, write the .bbox JSON directly and skip
the conversion. Either way the .bbox file is what loads into the
document model that extraction, tables and matching
operate on.
What must be installed
Section titled “What must be installed”| Component | Required for | Supplied by |
|---|---|---|
The kis CLI | Every command on this page | The platform install |
| The rules plugin | kis hocr2bbox, kis ocr, kis bbox, kis rules | A separate plugin install |
| An OCR engine that emits hOCR | Producing the input files | You — entirely your own installation |
Nothing else. No tessdata directory, no language pack, no model download, no locale
configuration. Text handling is character substitution only — no dictionary, no language model.
The acquisition path
Section titled “The acquisition path”your OCR engine ──▶ page.hocr ──▶ kis hocr2bbox ──▶ page.bbox (JSON word boxes) │ page.json (facts) ─────────────┬────┘ ├──▶ kis ocr page | kis ocr doc └──▶ POST /rule (same JSON, sent inline)kis ocr page and kis ocr doc pair a .bbox file with a same-basename .json facts file and
run rules over the pair. Those commands are covered in the
CLI reference; how the document reaches a rule is covered in
Rules over documents.
The .bbox file contents go under bbox, keyed by the document name the rule passes to
bbox.Get, next to the facts:
{ "name": "invoice-checks", "currency": "EUR", "bbox": {"page": {"pages": [{"number": 1, "width": 612, "height": 792, "words": []}]}}}hOCR ingestion
Section titled “hOCR ingestion”hOCR is HTML. It is parsed with a real HTML parser, not regex, and the parser is deliberately permissive about which producer emitted the file.
Recognised classes
Section titled “Recognised classes”This is the complete accepted vocabulary.
| Level | Classes |
|---|---|
| Page | ocr_page |
| Content area (recursed into) | 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 |
Any other class beginning with ocr produces an unrecognized_class warning in verbose mode;
its text is recovered only by the orphan-text fallback.
A page the parser accepts
Section titled “A page the parser accepts”<div class="ocr_page" id="page_1" title='image "/tmp/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>The page comes out 2550 × 3300; the word lands at {100,100,400,160} with confidence 0.95.
Mixed structure survives
Section titled “Mixed structure survives”Producers mix structure levels on one page. The parser runs a paragraph pass, then a bare-line
pass, then a bare-word pass, each skipping subtrees the earlier pass already consumed, then —
only if the page still has zero words — an orphan-text pass. A page containing an ocr_par, a
bare ocr_line and a bare ocrx_word side by side yields three words, one paragraph and two
lines, with nothing double-counted.
Title properties
Section titled “Title properties”An hOCR title is a ;-separated property list. Splitting honours double-quoted values, and
the property name must match as a whole token.
bbox 100 100 200 200 -> {100,100,200,200}image "/tmp/page_1.png"; bbox 0 0 2550 3300; ppageno 0 -> {0,0,2550,3300}image "/tmp/scan_bbox.png"; bbox 0 0 2550 3300 -> {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 bboxbboxes 1 2 3 4 -> no bboxbbox a b c d; ppageno 7 -> no bboxA malformed bbox yields nothing rather than scavenging digits from elsewhere in the title.
Confidence
Section titled “Confidence”| Property | Read as | Result |
|---|---|---|
x_wconf N | Integer 0–100 | N / 100 |
x_conf N | Float; divided by 100 if > 1 | N or N / 100 |
| Neither present | — | 1.0 |
| Word synthesised from fallback text | — | 0.0 |
Every word carries confidence in the 0–1 range whatever the producer.
Words with no box
Section titled “Words with no box”When a word element has no bbox, the parser synthesises the word using the nearest enclosing
element’s box — line, paragraph, content area, or page — and sets confidence to 0.0. If no
ancestor has a box either, the word gets a zero-size box at the origin. A cluster of words
sharing one identical oversized box, and (0,0,0,0) boxes, are both real possible output.
The word-box JSON format
Section titled “The word-box JSON format”kis hocr2bbox writes a .bbox file: JSON, indented two spaces, holding the serialised
document. This is also the format to emit directly if your engine does not produce hOCR.
{ "pages": [ { "number": 1, "width": 612, "height": 792, "words": [ { "text": "Borrower", "box": {"x0": 72, "y0": 100, "x1": 150, "y1": 115}, "confidence": 0.98, "page": 1 } ] } ]}words[].line_id is optional. lines and paragraphs arrays are optional and hold index
ranges into words and lines respectively — see the
document model for what those ranges mean.
Format detection
Section titled “Format detection”The extension is a hint, not a contract. The loader dispatches like this:
| Extension | Behaviour |
|---|---|
.json | JSON loader |
.hocr | hOCR parser |
.html, .htm | Sniffed for ocr_page, ocrx_word or ocr_word; hOCR if found, else JSON |
Anything else, including .bbox | JSON first, hOCR second |
This is why kis bbox subcommands accept a .hocr file even though their -i help string says
“path to bbox file”.
Converting hOCR
Section titled “Converting hOCR”kis hocr2bbox -i scan-page1.hocrWith no -o the output path is the input path with its extension replaced by .bbox.
| Flag | Default | Purpose |
|---|---|---|
-i, --input | — | hOCR file, or a directory of hOCR files |
-o, --output | Input location | Output file (single input) or directory (directory input) |
-w, --workers | 4 | Parallel workers; directory mode only |
-v, --verbose | false | Report content the parser could not place |
--clean | false | Apply the default text-cleanup profile |
--clean-form | false | Apply the form-aware cleanup profile |
kis hocr2bbox -i scan-page1.hocr -o out/page1.bboxkis hocr2bbox -i hocr/ -o bbox/ -w 8 --cleankis hocr2bbox -i hocr/ -o bbox/ --clean-formkis hocr2bbox -i hocr/ -vDirectory mode globs *.hocr in the given directory, non-recursively. .html and .htm files
are skipped even though the single-file path accepts them, and subdirectories are ignored.
Matching nothing is a warning with exit code 0, not an error. Directory mode creates -o if
needed; single-file mode does not create a missing parent directory.
Per-file failures in directory mode do not stop the run — every file is attempted, then the first error is returned.
Parse warnings
Section titled “Parse warnings”Warnings are the only visibility you have into content the parser could not place.
| Type | Meaning |
|---|---|
no_structure | No ocr_page found and fallback extraction found no words |
missing_bbox | A page or word element has no bbox |
no_bbox_fallback | No box available anywhere; a zero box was used |
empty_paragraph | Paragraph has text but no lines or words were extracted |
empty_line | Line has text but no words were extracted |
empty_word | Word element has no text content |
orphan_text | Text outside any word element; a fallback word was created |
unrecognized_class | An OCR class outside the accepted vocabulary |
missed_content | Post-hoc diff of source text against extracted word text |
missed_content compares all source text against all extracted word text, lowercased and
punctuation-stripped, ignoring single characters, and reports the first 10 missed words.
Each warning renders as [type] element: "text" (context: ...), with text trimmed and truncated
to 50 characters. Warnings carrying no text drop the : "text" part.
Text cleanup and normalisation
Section titled “Text cleanup and normalisation”Cleanup is pure text in, text out. It is character-level only — no dictionary, no language model, no language parameter.
| Option | Effect |
|---|---|
NormalizeUnicode | Unicode NFC |
FixQuotes | 23 quote, prime and guillemet variants — including backtick and acute — to ASCII ' or " |
FixDashes | 11 dash variants to - |
FixLigatures | fi/fl/ff/ffi/ffl/st, Æ/Œ/IJ/ꜳ/ꝏ, and symbol expansions such as No., (P), SM, TM, (R), (C) |
RemoveGarbage | Deletes 56 glyphs — bullets, arrows, box drawing, geometric shapes, stray diacritics, pilcrow, section sign |
NormalizeFormIndicators | Checkbox and radio glyphs to [ ], [x], ( ), (x) |
FixMisreads | Fullwidth ASCII to ASCII, Greek and Cyrillic homoglyphs to Latin, Roman numerals expanded, common Unicode space variants to a space, zero-width characters deleted |
RemoveStrayPunct | 16 X patterns to a single space |
CleanWhitespace | Whitespace collapse |
RemoveControlChars | Control characters removed |
AggressiveClean | Everything outside printable ASCII becomes a space; newlines and tabs survive |
The two profiles compose these options, applied in the order shown. RemoveGarbage reads
PreserveFormIndicators to decide whether checkbox and radio glyphs count as garbage.
Profiles
Section titled “Profiles”| Option | Default profile | Form-aware profile |
|---|---|---|
NormalizeUnicode, FixQuotes, FixDashes, FixLigatures | on | on |
RemoveGarbage, FixMisreads, CleanWhitespace, RemoveControlChars | on | on |
PreserveFormIndicators | on | on |
NormalizeFormIndicators | off | on |
RemoveStrayPunct | off | off |
AggressiveClean | off | off |
--clean selects the default profile; --clean-form selects the form-aware profile.
Cleanup runs per word
Section titled “Cleanup runs per word”Cleanup is applied to each word’s text, then each line’s text, then each paragraph’s text. Three consequences:
- Boxes are never adjusted. A word whose text is emptied by garbage-stripping stays in the output as an empty-text box.
- Word text and line text are cleaned independently. A line’s text was joined from the raw words at parse time, so after cleanup a line’s text is not necessarily its cleaned words rejoined.
RemoveStrayPunctmatches only a punctuation character with a space on both sides, so it is a no-op on word text — a single word has no spaces — but it does fire on line and paragraph text, where words were joined with spaces.
Engine output shapes
Section titled “Engine output shapes”No surface selects an OCR engine: there is no CLI flag, no file format and no request field that
names one. Convert your recogniser’s output to hOCR or to word-box JSON yourself, then supply it
the way every other document is supplied — a path to kis ocr page, or inline under bbox on a
request. These three common shapes map onto word-box JSON, and the notes say what your conversion
must handle.
| Source | Shape it emits | What your conversion must do |
|---|---|---|
| PaddleOCR | Four-corner polygons, per-page result arrays | Reduce each polygon to an axis-aligned box; drop items with fewer than 4 corners |
| Tesseract TSV | Row records with level, position, size, conf | Keep only level-5 (word) rows with non-empty text; divide conf by 100 |
| Docling | Word records with bbox [x0,y0,x1,y1] and score | Require at least 4 bbox values; score becomes confidence |
PaddleOCR and Tesseract report page width and height. Docling does not — carry the page size from
your own pipeline, or the page arrives sized 0 × 0.
Page dimensions
Section titled “Page dimensions”Page width and height are read from the ocr_page bbox, or from width and height in
word-box JSON. If the hOCR has no ocr_page bbox, its page title is malformed, or the JSON page
omits the dimensions, both are 0.
Page analysis seeds the statistics from the page declaration, and falls back to the bounding extent of the page’s words when the declaration is zero, flagging that the fallback was used. Percentage-based spatial queries scale by the statistics, not by the raw page record, so they still resolve. The fallback under-estimates the physical page — margins beyond the last word are lost — so percentage regions come out over-scaled. It is a bounded approximation rather than a dead query.
Concurrency
Section titled “Concurrency”Analysis runs in full for every execution that carries a document, on both surfaces, and caches nothing between executions. Every detector — table, field, checkbox, region — runs on every page, and none of them can be turned off from the CLI or from the request.
Where the two surfaces differ is how many documents are in flight at once, and that is a CLI concern: the service analyses exactly one request’s documents at a time, on the request goroutine.
| Where | Flag | Default | Effect |
|---|---|---|---|
kis hocr2bbox -w | -w / --workers | 4 | Directory mode only; ignored for a single file |
kis ocr page -w | -w / --workers | 1 | Fully serial; clamped to the number of resolved inputs |
kis ocr doc | — | — | No workers flag; the whole corpus becomes one document |
rules.svc | — | — | One analysis per request, synchronous |
Errors
Section titled “Errors”| Message | Cause |
|---|---|
Error: '<command>' is not installed. | The rules plugin binary is absent from every search location, or present without an executable bit |
failed to stat input path | -i does not exist |
loading hocr failed from given input file | The path could not be opened, or the HTML parser failed |
failed to create output directory | Directory mode could not create -o |
no hocr files found in directory | Directory mode matched no *.hocr; warning, exit code 0 |
writing json to output file path failed | Permissions, or a missing parent directory in single-file mode |
some files failed to process | One or more files failed in directory mode; the first error is returned |
The full table is in Error reference; diagnosis steps are in Troubleshooting.
Continue with
Section titled “Continue with”- Document model — what pages, lines, words and boxes mean
- Extraction — fields, checkboxes and regions
- Tables — table and master-detail detection
- Matching and confidence — fuzzy matching and OCR confusion handling
- Rules over documents — reaching a document from a rule
- CLI reference —
kis ocr pageandkis ocr doc