/** * The OCR seam. Everything above this interface works in terms of page text and * word boxes, so the concrete engine is swappable without touching the parsers, * the matcher, or the schema. * * The shipped implementation is self-hosted Tesseract (see tesseract.provider). * That choice is evidence-based rather than assumed: run against 46 pages of * real scanned CFE, CESPT and Telnor statements, it identified the provider on * 46/46 and extracted a usable account reference on 43/46, and on a later * corpus of 19 scanned municipal predial receipts it read the provider on * 19/19 and an identifier on 18/19 — well past the bar for a queue whose whole * point is that a human confirms every row. A * managed document-extraction API (Textract, Document Intelligence, Document * AI) fits behind this same interface if per-page accuracy ever proves * insufficient, with no schema change — but at 300+ pages/month/company it * would carry a real recurring cost for accuracy that is not currently the * bottleneck. */ /** One OCR'd word, with where it sits on the page. */ export interface OcrWord { text: string; /** Pixel box in the rendered page image. */ left: number; top: number; width: number; height: number; /** Engine confidence for this word, 0..1. */ confidence: number; } export interface OcrPage { /** Full page text, reading order, newline-separated. */ text: string; /** * Word boxes. Needed because several of the real layouts are *tables* — the * CESPT "RECIBO" prints `No. DE CUENTA` as a column header with the value in * the row beneath it, which line-oriented text cannot associate. Parsers fall * back to geometry for exactly those fields. */ words: OcrWord[]; /** Mean word confidence across the page, 0..1. */ confidence: number; } export interface OcrProvider { /** True when the engine is actually usable in this deployment. */ available(): Promise; /** Split a PDF into one rendered page image per page. */ renderPages(pdf: Buffer): Promise; /** OCR a single rendered page image. */ recognize(pageImage: Buffer): Promise; /** * Read a PDF's own text layer, one entry per page, `null` where the page has * none worth using. * * Not every statement is a scan. The gas company e-mails born-digital CFDI * invoices whose text is already exact and already positioned — running those * through a rasteriser and a character recogniser can only lose information * (one sample turned `MEDIDOR: VM01014426` into `ar (LTR): 014420`) while * costing about a minute of CPU per page for the privilege. Where the layer * exists it is strictly better input for the same parsers, so it is tried * first and OCR remains the fallback for genuine scans. * * Positions are reported in the same pixel space `recognize` uses, so the * geometric helpers in the parsers work unchanged on either source. */ textPages(pdf: Buffer): Promise<(OcrPage | null)[]>; } export const OCR_PROVIDER = Symbol("OCR_PROVIDER");