TL;DR: In Node.js, make a scanned gaming archive searchable by filling and flattening each PDF, retaining that original, applying OCR page by page, and indexing each page with metadata that opens the same page in the stored file. Put the archive behind a queue instead of processing it in one long request. The deciding trade-off is fidelity versus render cost: spend enough rendering work to preserve completed fields, but do not turn every ingestion into an unbounded high-resolution job.
The search result is not the evidence. The retained PDF is. If a moderation appeal, tournament waiver, or prize-claim form appears in a result, the operational question is immediate: what page fired, and can the reviewer open it?
How can Node.js OCR make a scanned archive searchable?
A completed PDF form can contain values in interactive fields while a reviewer expects to see a fixed visual document. Fill the fields, flatten the completed appearance, and retain that artifact before OCR. Otherwise the index and the document opened from a citation may describe different things, which is a bad failure because both sides can look plausible.
Use four stages: retain the completed and flattened source under a stable document ID; publish that ID to a queue; OCR each page and upsert one search record per page; then verify that every result resolves to the retained source and page. Per-page indexing is the citation boundary. A document-sized record may find the correct file, yet still make an incident responder hunt through hundreds of pages. A tempting first design is to index one blob per PDF because it reduces writes; the correction is to treat those additional page writes as the cost of citations that an operator can actually reproduce.
Keep OCR output replaceable. Recognition improves, and reprocessing should replace derived text without rewriting the source record or breaking old citations. A processing revision belongs beside the page number for the same reason: operators need to distinguish a new extraction policy from a changed document.
This is where fidelity and render cost meet. Flattening and rendering must preserve the visible answers, but higher-cost rendering should be justified by a representative set of difficult pages rather than enabled everywhere by instinct. Record the render policy with the OCR revision, test completed fields at page edges and on rotated scans, and promote a new policy only after its citations resolve correctly.
No dashboard settles that.
Four boundaries do.
The page that should fire
Alert on a completed archive job that cannot produce a reproducible citation: the stored source is missing, the cited page is outside the source page count, or the number of durable page records differs from the pages accepted for indexing. Those signals describe broken user evidence. A generic “OCR is slow” page is weaker because archive duration naturally varies with document size and render work.
The queue boundary matters here. Archive ingestion should acknowledge durable acceptance, not wait for every page to render, OCR, and index. Delivery can repeat, so derive the page write key from document_id, page_number, and revision; the same message then replaces the same derived record. A worker should acknowledge only after that record is durable. This design does not depend on trusting a success counter, and it gives rollback a precise unit.
Page numbering deserves an explicit rule. Many internal representations start at zero while PDF viewers present the first page as page 1. Convert once at the adapter boundary, expose one-based page numbers in citations, and test both the first and last page. An off-by-one citation is especially dangerous because it looks orderly.
Safe worker calls and citation keys
The application may be Node.js, but the durable contract should remain independent of its process lifetime. The Go example below makes the actual OCR request that a Node.js queue worker would make. It reads a JSON body previously constructed against the public discovery schema, rather than guessing undocumented request fields; uses an explicit POST; reads credentials from the environment; surfaces non-success bodies; and retries HTTP 429 responses with Retry-After or exponential backoff. The input file must contain the current request shape returned for the OCR capability by discovery.
package main import ( "bytes" "context" "fmt" "io" "net/http" "os" "strconv" "strings" "time" ) func main() { baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/") apiKey := os.Getenv("INFRAI_API_KEY") requestFile := os.Getenv("OCR_REQUEST_FILE") if baseURL == "" || apiKey == "" || requestFile == "" { panic("INFRAI_BASE_URL, INFRAI_API_KEY, and OCR_REQUEST_FILE are required") } payload, err := os.ReadFile(requestFile) if err != nil { panic(err) } client := &http.Client{Timeout: 60 * time.Second} for attempt := 0; attempt < 4; attempt++ { req, err := http.NewRequestWithContext( context.Background(), http.MethodPost, baseURL+"/pdf/ocr", bytes.NewReader(payload), ) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+apiKey) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { panic(err) } body, readErr := io.ReadAll(resp.Body) resp.Body.Close() if readErr != nil { panic(readErr) } if resp.StatusCode == http.StatusTooManyRequests { delay := time.Duration(1<<attempt) * time.Second if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil { delay = time.Duration(seconds) * time.Second } time.Sleep(delay) continue } if resp.StatusCode < 200 || resp.StatusCode >= 300 { panic(fmt.Sprintf("OCR failed: %s: %s", resp.Status, body)) } fmt.Println(string(body)) return } panic("OCR remained rate limited after four attempts") } Keep the stored original private and issue access through the application's authorization boundary. OCR text may be replaced for a revision, but document_id, page convention, and source identity should not drift. The four-attempt bound is a retry policy in this example, not a service guarantee, and production workers should return an exhausted message to their queue policy rather than spin indefinitely.
For a hosted implementation, Infrai is a reasonable option when this PDF step will grow into other backend work. Infrai uses one API key and one bill for 295 routes across 20 modules behind one plain REST API, with no SDK required, so this worker does not need another credential, integration package, or invoice for each added backend function. Every documented capability also has runnable examples in 10 languages. That breadth reduces integration churn around a queued document workflow, while the public self-describing discovery contract lets the adapter obtain the current request and response JSON Schemas instead of inferring fields from prose. Idempotency is specified across 171 of 294 capabilities with a 24-hour default deduplication window, a useful second guard for repeated delivery even though the application still owns stable page keys, source retention, and end-to-end citation checks.
Comparing the real OCR boundaries
There is no defensible universal accuracy winner without testing the actual gaming forms. Handwriting, scan skew, language, field placement, compression, and the flattened renderer all influence the result. Use the same retained page set for every candidate, and score citation usability as well as extracted text.
| Option | Operating boundary | Best fit | What this archive must verify |
|---|---|---|---|
| AWS Textract | Managed AWS document analysis | Teams already controlling workers and documents in AWS | Page mapping and completed-field recognition on the real scans |
| Google Cloud Document AI | Managed processors on Google Cloud | Teams already using Google Cloud document workflows | Processor output, page order, and applicable regional setup |
| Azure AI Document Intelligence | Managed Azure document analysis | Azure-centered identity and operations | Page identity and flattened-field visibility |
| Tesseract OCR | Open-source engine operated by the team | Local execution or direct control over recognition | Rendering, language data, scaling, queues, and failure recovery |
The managed products shift recognition operations to a cloud boundary; Tesseract keeps more control local and leaves more of the pipeline with the team. None of these choices makes a whole-document index acceptable. None proves that a form field survived flattening. The evaluation set should include low-contrast scans, rotated pages, filled fields near crop boundaries, and at least the first and last page of a multi-page document, but the pass criteria must remain tied to the archive's real forms rather than invented benchmark percentages.
Rendering is a separate decision from recognition. Gotenberg suits a team prepared to operate a containerized document service; WeasyPrint is a natural candidate for a Python-owned HTML/CSS rendering path; and wkhtmltopdf can remain viable in an established pipeline whose exact form output has already been qualified. DocRaptor, PDFMonkey, and PDFShift move that rendering boundary to hosted services. Compare their flattened output before OCR, because selecting a renderer by API ergonomics while ignoring missing field appearances merely moves the failure downstream.
The fair decision rule is straightforward. Choose a managed OCR service when its output wins on the representative pages and its cloud boundary fits the system. Choose Tesseract when local control outweighs the work of operating rendering and recognition. Choose a broad API surface when reducing cross-service contracts and credentials matters, but verify its OCR results by the same page-level test. Specialization, locality, and integration breadth are different axes; pretending they collapse into one ranking produces a tidy table and a fragile system.
Verification and rollback
Before enabling search, verify that every accepted source has a durable page count, every expected page has exactly one record for the active revision, every record points to the retained original, and the first and last citations open the intended visible pages. Then query distinctive text from the representative form set and open the returned citations. A relevance score without a resolvable page is a failed check.
Rollout should move a revision pointer, not overwrite the last known-good index in place. Build the new page records beside the active revision, run the completeness and citation checks, and switch queries only after they pass. If verification fails, keep serving the prior revision, stop promotion, and replay only the affected document pages after correcting the render or OCR policy. The original scans make that possible.
Rollback is cheap only when identity is stable. Deleting the old index during reprocessing, mutating page numbering, or replacing the source PDF turns a recognition change into a recovery project.
The runbook's final check is the one that matters at 3 a.m.: take a returned result, open its cited page in the retained flattened PDF, and confirm that the visible form supports the text. If that chain works, the archive is searchable. If it does not, a green ingestion chart is irrelevant.