Skip to content
Talk to our solutions team

Text acquisition API

Reference for the packages that turn OCR engine output into the spatial document model the Rules block queries: ocr (parsing and adapters), ocrclean (text normalisation), packs (domain vocabulary and classifiers), and parallel (page-level concurrency). These are the signatures behind kis hocr2bbox and kis ocr and the document capabilities described under Documents and Operating the service.

ocr.Document is what every downstream query, extraction, and rule sees. It is also the on-disk shape of a .bbox file.

type Document struct {
Pages []Page `json:"pages"`
}
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"`
}
FieldTypeJSONMeaning
TextstringtextToken text
Boxgeometry.BoxboxAxis-aligned bounding box
Confidencefloat64confidence0..1, normalised from the source
PageintpagePage number this word belongs to
LineIDintline_id,omitemptyLine grouping, when the source provides one

geometry.Box carries X0, Y0 (top-left) and X1, Y1 (bottom-right), all float64, serialised as x0/y0/x1/y1.

FieldTypeJSONMeaning
IDintidLine index on the page
TextstringtextMember words joined by a single space
Boxgeometry.BoxboxUnion of the member word boxes
Confidencefloat64confidence,omitemptyMean of member word confidences
WordStartintword_startFirst index into Page.Words
WordEndintword_endOne past the last index (exclusive)
FieldTypeJSONMeaning
IDintidParagraph index on the page
TextstringtextMember lines joined by newline
Boxgeometry.BoxboxUnion of the member line boxes
Confidencefloat64confidence,omitemptyMean of member line confidences
LineStartintline_startFirst index into Page.Lines
LineEndintline_endOne past the last index (exclusive)
func (d *Document) GetPage(pageNum int) []Word
func (d *Document) AllWords() []Word
func (d *Document) PageCount() int
func (d *Document) UnescapeText()
func (d *Document) CleanDocument()
func (d *Document) CleanDocumentWithOptions(opts ocrclean.CleanOptions)
func (p *Page) CleanPage()
func (p *Page) CleanPageWithOptions(opts ocrclean.CleanOptions)
func (w Word) EstimateCharWidth() float64
func NewWord(text string, x0, y0, x1, y1, confidence float64, page int) Word
SymbolReturnsNotes
GetPage[]WordMatches on Page.Number; nil when absent
AllWords[]WordConcatenates every page in slice order
PageCountintlen(d.Pages)
UnescapeTextApplies HTML entity unescaping to every word text, in place
CleanDocumentCleanDocumentWithOptions(ocrclean.DefaultOptions())
CleanPageWithOptionsRewrites Word.Text, then Line.Text, then Paragraph.Text
EstimateCharWidthfloat64Box.Width() / len(Text); 0 for empty text
NewWordWordNormalises the box so X0 <= X1, Y0 <= Y1
func Load(path string) (*Document, error)
func LoadJSON(path string) (*Document, error)
func LoadHOCR(path string) (*Document, error)
func LoadHOCRWithOptions(path string, opts ParseOptions) (*ParseResult, error)
func ParseHOCR(r io.Reader) (*Document, error)
func ParseHOCRWithOptions(r io.Reader, opts ParseOptions) (*ParseResult, error)
func LoadWithCleanup(path string) (*Document, error)
func LoadWithCleanupOptions(path string, opts ocrclean.CleanOptions) (*Document, error)
func LoadHOCRWithCleanup(path string) (*Document, error)
func LoadHOCRWithCleanupOptions(path string, opts ocrclean.CleanOptions) (*Document, error)
FunctionInputReturns
LoadAny supported path, format sniffed*Document
LoadJSONJSON word-box file (.bbox, .json)*Document
LoadHOCRhOCR file*Document, warnings discarded
LoadHOCRWithOptionshOCR file + ParseOptions*ParseResult
ParseHOCRio.Reader*Document, warnings discarded
ParseHOCRWithOptionsio.Reader + ParseOptions*ParseResult
LoadWithCleanupAny supported path*Document, DefaultOptions() cleanup applied
LoadWithCleanupOptionsAny supported path + CleanOptions*Document, cleaned
LoadHOCRWithCleanuphOCR file*Document, DefaultOptions() cleanup applied
LoadHOCRWithCleanupOptionshOCR file + CleanOptions*Document, cleaned
ExtensionBehaviour
.jsonJSON loader
.hocrhOCR parser
.html, .htmRead and sniffed for ocr_page, ocrx_word or ocr_word; hOCR if found, otherwise JSON
anything else (including .bbox)JSON first; hOCR on failure

The extension is a hint, not a contract — .bbox is resolved by content, and an hOCR file is accepted anywhere a bbox file is.

LevelClasses
Pageocr_page
Content area (recursed into)ocr_carea, ocr_photo, ocr_graphic, ocr_separator, ocr_noise
Blockocr_par, ocr_textfloat, ocr_caption, ocr_header, ocr_footer
Lineocr_line, ocrx_line, ocr_textline, ocr_header_line
Wordocrx_word, ocr_word
Characterocrx_cinfo, ocr_cinfo

That is the complete accepted vocabulary. Per page the parser runs a paragraph pass, a bare-line pass, and a bare-word pass, each skipping subtrees the earlier passes consumed; an orphan-text pass runs only if the page still has zero words.

An hOCR title is a ;-separated property list, for example image "/tmp/page_1.png"; bbox 0 0 2550 3300; ppageno 0. Splitting honours double-quoted values, and the property name must match as a whole token, so bboxes and a filename containing bbox are both rejected. A non-numeric bbox yields no box rather than scavenging digits from elsewhere in the title.

PropertyRead asResult
x_wconf Ninteger 0–100N / 100
x_conf Nfloatdivided by 100 when > 1
neither presentconfidence 1.0
word synthesised from fallback textconfidence 0.0
type ParseOptions struct {
Verbose bool
}
type ParseResult struct {
Document *Document
Warnings []ParseWarning
}
type ParseWarning struct {
Type string
Element string
Text string
Context string
}
func (w ParseWarning) String() string
FieldMeaning
ParseOptions.VerboseAccumulate warnings. The only option.
ParseWarning.TypeWarning kind, from the table below
ParseWarning.ElementElement tag or class that triggered it
ParseWarning.TextAssociated text, truncated to 50 characters with a ... suffix
ParseWarning.ContextWhere in the tree it happened

String() formats as [type] element: "text" (context: ctx), dropping the quoted text when it is empty.

TypeEmitted when
missing_bboxAn element carries no parseable bbox
no_bbox_fallbackA word took an ancestor’s box
no_structureNo ocr_page found and fallback extraction found no words
empty_paragraphA paragraph element yielded no lines
empty_lineA line element yielded no words
empty_wordA word element yielded no text
orphan_textText rescued outside any recognised structure
unrecognized_classA class starting with ocr that is not in the vocabulary
missed_contentPost-hoc diff of source text against extracted word text

missed_content compares punctuation-stripped, lowercased text, ignores single characters, and reports at most the first 10 missed words.

Three adapters build a Document from already-parsed engine output. They are in-process conversions over the structs below.

func FromPaddleOCR(result PaddleOCRResult, pageWidth, pageHeight float64) *Document
func FromTesseract(words []TesseractWord, pageWidth, pageHeight float64) *Document
func FromDocling(words []DoclingWord) *Document
AdapterCompletenessBehaviour
FromPaddleOCRCompleteReduces each 4-corner polygon to an axis-aligned box; items with fewer than 4 corners are skipped. Pages come out in order, numbered from 1.
FromTesseractPartialKeeps only Level == 5 (word-level) rows with non-empty text; divides Confidence by 100. Page order is nondeterministic.
FromDoclingPartialReads Bbox as [x0,y0,x1,y1] (needs at least 4 values) and Score as confidence. Page order is nondeterministic, and page dimensions stay zero.
type PaddleOCRResult struct {
Results [][]PaddleOCRItem `json:"results"`
}
type PaddleOCRItem struct {
Boxes [][]float64 `json:"boxes"`
Text string `json:"text"`
Confidence float64 `json:"confidence"`
}

Results is one slice per page; Boxes is [[x0,y0],[x1,y1],[x2,y2],[x3,y3]].

type TesseractWord struct {
Level int `json:"level"`
PageNum int `json:"page_num"`
BlockNum int `json:"block_num"`
ParNum int `json:"par_num"`
LineNum int `json:"line_num"`
WordNum int `json:"word_num"`
Left float64 `json:"left"`
Top float64 `json:"top"`
Width float64 `json:"width"`
Height float64 `json:"height"`
Confidence float64 `json:"conf"`
Text string `json:"text"`
}

Left/Top/Width/Height become the box; LineNum becomes Word.LineID.

type DoclingWord struct {
Text string `json:"text"`
Bbox []float64 `json:"bbox"`
Score float64 `json:"score"`
PageNo int `json:"page_no"`
}
type HOCRWord struct {
XMLName xml.Name `xml:"span"`
Class string `xml:"class,attr"`
Title string `xml:"title,attr"`
Text string `xml:",chardata"`
}

HOCRWord is an XML-tag alternative to the HTML parse path. No function in the package consumes it.

The ocrclean package is pure text in, text out. It is character-level only — no dictionary, no language model, no language parameter.

type CleanOptions struct {
NormalizeUnicode bool
FixQuotes bool
FixDashes bool
FixLigatures bool
RemoveGarbage bool
FixMisreads bool
CleanWhitespace bool
RemoveControlChars bool
RemoveStrayPunct bool
PreserveLineBreaks bool
AggressiveClean bool
PreserveFormIndicators bool
NormalizeFormIndicators bool
}
FieldDefaultOptions()FormAwareOptions()Applies
NormalizeUnicodetruetrueUnicode NFC
FixQuotestruetrue25 quote, prime and guillemet variants to ASCII ' or "
FixDashestruetrue11 dash variants to -
FixLigaturestruetruefi/fl/ff/ffi/ffl/st, AE/OE/IJ, and symbol expansions (No., (P), SM, TM, (R), (C))
RemoveGarbagetruetrueDeletes ~55 bullet, arrow, box-drawing and geometric glyphs
FixMisreadstruetrueFullwidth ASCII, Greek/Cyrillic homoglyphs, Roman numerals, space variants, zero-width removal
CleanWhitespacetruetrueCollapses runs, trims lines, caps blank lines at two
RemoveControlCharstruetrueDrops control runes, keeping \n, \t, \r
RemoveStrayPunctfalsefalse16 X patterns to a space
PreserveLineBreakstruetrueNothing — see below
AggressiveCleanfalsefalseEverything outside printable ASCII becomes a space
PreserveFormIndicatorstruetrueExempts checkbox and radio glyphs from garbage removal
NormalizeFormIndicatorsfalsetrueRewrites those glyphs to [ ], [x], ( ), (x)

FormAwareOptions() is DefaultOptions() with NormalizeFormIndicators set — that single field is the only difference.

func DefaultOptions() CleanOptions
func FormAwareOptions() CleanOptions
func CleanWithOptions(text string, opts CleanOptions) string
func CleanOCR(text string) string

CleanWithOptions applies steps in a fixed order: NormalizeUnicode, FixQuotes, FixDashes, FixLigatures, RemoveGarbageCharsWithOptions, NormalizeFormIndicators, FixCommonMisreads, RemoveStrayPunctuation, CleanWhitespace, RemoveControlChars, AggressiveCleanup.

CleanOCR is a fixed pipeline that ignores options entirely: NormalizeUnicode, FixQuotes, FixDashes, FixLigatures, RemoveGarbageChars, FixCommonMisreads, CleanWhitespace, RemoveControlChars.

func NormalizeUnicode(text string) string
func FixQuotes(text string) string
func FixDashes(text string) string
func FixLigatures(text string) string
func FixCommonMisreads(text string) string
func RemoveGarbageChars(text string) string
func RemoveGarbageCharsWithOptions(text string, preserveFormIndicators bool) string
func NormalizeFormIndicators(text string) string
func CleanWhitespace(text string) string
func RemoveControlChars(text string) string
func RemoveStrayPunctuation(text string) string
func AggressiveCleanup(text string) string
func RemoveRepeatedChars(text string) string
func FixBrokenWords(text string) string
func StripPunctuation(s string) string
FunctionDoesIn a profile
NormalizeUnicodeNFC normalisationyes
FixQuotesQuote, prime and guillemet foldingyes
FixDashesDash folding to -yes
FixLigaturesLigature and digraph expansionyes
FixCommonMisreadsFullwidth and homoglyph foldingyes
RemoveGarbageCharsGarbage glyph removal, form indicators preservedyes, via the options variant
RemoveGarbageCharsWithOptionsSame, with explicit preservation flagyes
NormalizeFormIndicatorsCheckbox and radio glyphs to ASCIIform-aware profile only
CleanWhitespaceWhitespace normalisation and trimmingyes
RemoveControlCharsControl rune removalyes
RemoveStrayPunctuationIsolated punctuation to a spacedeclared, off by default
AggressiveCleanupNon-printable-ASCII to space, then CleanWhitespaceopt-in
RemoveRepeatedCharsRuns of 4+ identical runes collapsed to 2no
FixBrokenWordsRejoins word-\nwordno
StripPunctuationTrims leading and trailing punctuation and quotesno

FormIndicators is the exported map driving the last three form functions:

var FormIndicators = map[string]string{ /* glyph -> "[ ]" | "[x]" | "( )" | "(x)" */ }

Circles map to ( ) / (x); squares, ballot boxes, check marks and ballot crosses map to [ ] / [x].

A pack registers domain vocabulary into the shared registries at package init. Packs are domain packs — there is no language pack, no per-language data, and no download step.

func field.RegisterPack(p field.Pack)
func pattern.RegisterPack(p pattern.Pack)
func text.RegisterVocabulary(name string, v *text.Vocabulary)
func special.AddSignatureAnchors(anchors ...string)
func document.RegisterDoctype(spec document.DoctypeSpec)

Read them back with field.RegisteredFieldDefs(), text.RegisteredVocabularies() and text.MergedVocabulary().

func Register()
func Unregister()
func Doctypes() []document.DoctypeSpec
func CommonMortgageFields() []field.FieldDef
func Vocabulary() *text.Vocabulary
var SignatureAnchors []string
SymbolReturnsNotes
RegisterInstalls fields, patterns, vocabulary, signature anchors and doctypes. Runs automatically at package init; guarded by sync.Once, so repeat calls are no-ops.
UnregisterTest-only teardown
Doctypes[]document.DoctypeSpecThe 8 classifiers, for passing explicitly to document.Classify
CommonMortgageFields[]field.FieldDefThe 13 field definitions, for driving an extraction loop directly
Vocabulary*text.VocabularyA fresh vocabulary, seeded with terms and known corrections
SignatureAnchors[]string8 signature-label substrings, added to the domain-neutral defaults

Doctype classifiers registered: loan_estimate, closing_disclosure, urla_borrower, urla_additional_borrower, appraisal, title_commitment, promissory_note, deed_of_trust.

Field definitions registered under pack name mortgage: Borrower Name, Property Address, Loan Amount, Interest Rate, SSN, Date of Birth, Phone, Email, Monthly Income, Employer, NMLS ID, MERS MIN, APN.

Pattern types registered under pack name mortgage:

const (
PatternNMLS pattern.PatternType = "nmls"
PatternAPN pattern.PatternType = "apn"
PatternMERS pattern.PatternType = "mers"
PatternCaseNumber pattern.PatternType = "case_number"
PatternLoanNumber pattern.PatternType = "loan_number"
PatternCreditScore pattern.PatternType = "credit_score"
PatternMortgageRatio pattern.PatternType = "mortgage_ratio"
PatternInterestRate pattern.PatternType = "interest_rate"
PatternBookPage pattern.PatternType = "book_page"
PatternRecordingNumber pattern.PatternType = "recording_number"
)

PatternMortgageRatio covers LTV, CLTV and DTI with one generic percentage regex — the distinction comes from the surrounding label, so use a field definition when you need a specific one.

The vocabulary carries mortgage terms across parties, property, loan, income, assets and closing, plus 8 hand-written corrections: 8orrowerBorrower, 1ncomeIncome, Emp1oyerEmployer, Principa1Principal, Ba1anceBalance, Tota1Total, Monthl yMonthly, Annua1Annual.

The parallel package fans pages out over goroutines. Every workers argument follows the same rule: a value <= 0 means runtime.NumCPU().

type ProcessorConfig struct {
Workers int
DetectTables bool
DetectFields bool
DetectCheckboxes bool
DetectRegions bool
MergeWords bool
MergeConfig text.MergeConfig
}
func DefaultProcessorConfig() ProcessorConfig
func NewProcessor() *Processor
func NewProcessorWithConfig(config ProcessorConfig) *Processor
func (p *Processor) ProcessDocument(doc *ocr.Document) (*DocumentResult, error)
func (p *Processor) ProcessDocumentContext(ctx context.Context, doc *ocr.Document) (*DocumentResult, error)
FieldDefaultEffect
Workers0NumCPU()Goroutine count
DetectTablestrueRuns the table detector per page
DetectFieldstrueRuns the field detector per page
DetectCheckboxestrueRuns the checkbox detector per page
DetectRegionstrueBuilds a region.PageLayout per page
MergeWordstrueMerges words into blocks; when false each word becomes its own block
MergeConfigtext.DefaultMergeConfig()Gap multiplier 0.5, pattern merge on, dictionary merge off

NewProcessor() uses DefaultProcessorConfig(). ProcessDocument delegates to ProcessDocumentContext with context.Background(). Both return the first error encountered — a cancelled context or a failing page aborts the whole document and returns nil results.

Each page is stat-computed, then seeded with DocStats.SeedPageDims, which falls back to the words’ maximum extent when the page declares no dimensions and sets PageDimsDerived to report the reduced precision.

type PageResult struct {
PageNum int
Blocks []text.Block
Lines []text.Line
Tables []table.Table
Fields []field.Field
Checkboxes []checkbox.Checkbox
Layout *region.PageLayout
Stats text.DocStats
Error error
}
type DocumentResult struct {
PageResults []*PageResult
Blocks []text.Block
Lines []text.Line
Tables []table.Table
Fields []field.Field
Checkboxes []checkbox.Checkbox
PageLayouts []*region.PageLayout
Stats text.DocStats
SpatialIndex *geometry.SpatialIndex[text.Block]
}
func (dr *DocumentResult) PageCount() int
func (dr *DocumentResult) GetPageResult(pageNum int) *PageResult

DocumentResult concatenates every per-page slice in page order and builds a spatial index over the combined blocks. GetPageResult matches on PageNum and returns nil when absent.

type PageFunc func(page ocr.Page, pageIdx int) (any, error)
type PageFuncResult struct {
PageNum int
PageIdx int
Result any
Error error
}
func ForEachPage(doc *ocr.Document, fn PageFunc, workers int) []PageFuncResult
func ForEachPageContext(ctx context.Context, doc *ocr.Document, fn PageFunc, workers int) []PageFuncResult
func ForEachPageOrdered(doc *ocr.Document, fn PageFunc, workers int) ([]any, []error)
func ForEachPageWithProgress(doc *ocr.Document, fn PageFunc, workers int, onProgress func(progress *Progress)) []PageFuncResult
func MapPages[T any](doc *ocr.Document, fn func(page ocr.Page) (T, error), workers int) ([]T, error)
func FilterPages(doc *ocr.Document, predicate func(page ocr.Page) bool, workers int) []ocr.Page
func ReducePages[T any](doc *ocr.Document, initial T, fn func(acc T, page ocr.Page) T) T
func ParallelReducePages[T any](doc *ocr.Document, initial T, mapFn func(page ocr.Page) T, combineFn func(a, b T) T, workers int) T
FunctionReturnsNotes
ForEachPage[]PageFuncResultResults indexed by page position, so order is preserved even though execution is not
ForEachPageContext[]PageFuncResultA cancelled context fills remaining slots with ctx.Err() rather than aborting
ForEachPageOrdered([]any, []error)Splits ForEachPage output into parallel value and error slices
ForEachPageWithProgress[]PageFuncResultWraps fn, incrementing a Progress and invoking onProgress after each page; onProgress may be nil
MapPages([]T, error)Returns the full result slice and the first non-nil error
FilterPages[]ocr.PagePredicate evaluated in parallel, output kept in document order
ReducePagesTSequential, despite the package. No workers argument.
ParallelReducePagesTParallel map phase, sequential combine phase in page order

onProgress in ForEachPageWithProgress is called from every worker goroutine. Anything it touches must be safe for concurrent use.

type BatchProcessor struct {
BatchSize int
Workers int
}
func NewBatchProcessor(batchSize, workers int) *BatchProcessor
func (bp *BatchProcessor) ProcessBatches(doc *ocr.Document, pageFn PageFunc, batchFn func(batchNum int, results []PageFuncResult) error) error
type StreamProcessor struct {
Workers int
BufferSize int
}
func NewStreamProcessor(workers, bufferSize int) *StreamProcessor
func (sp *StreamProcessor) Process(ctx context.Context, doc *ocr.Document, fn PageFunc) <-chan PageFuncResult
ConstructorArgument<= 0 becomes
NewBatchProcessorbatchSize100
NewBatchProcessorworkersruntime.NumCPU()
NewStreamProcessorworkersruntime.NumCPU()
NewStreamProcessorbufferSizeworkers * 2

ProcessBatches slices the document into BatchSize-page sub-documents, processes each with ForEachPage, and calls batchFn after each batch with the absolute batch number and its results. A non-nil return from batchFn stops processing and is returned. pageFn receives the absolute page index, not the index within the batch.

Process returns a buffered channel and closes it when every worker finishes. Cancelling the context stops workers, and the channel closes with results already sent still readable.

type Progress struct {
Total int
Completed int
Failed int
}
func NewProgress(total int) *Progress
func (p *Progress) Increment(success bool)
func (p *Progress) Percent() float64
SymbolBehaviour
NewProgressSets Total; Completed and Failed start at zero
IncrementMutex-guarded; always increments Completed, and Failed when success is false
PercentCompleted / Total * 100; returns 100 when Total is zero

Read Total, Completed and Failed through Percent or from inside onProgress. The fields are guarded by an unexported mutex, so reading them directly from another goroutine races.