Pages & Words#
Once you have an H instance, the page's content and
whitespace geometry hang off it directly. All coordinates are pixels on the
1400 × 2100 page image (see Concepts → Coordinate system).
H.page#
H.page: {
number: number;
width: number;
height: number;
body: Bbox | null; // type-block (printed area) bbox
valid: Bbox | null; // OCR-valid region, usually == body
imageUrl: string; // always set — load this with your renderer
}
body / valid are null only if the page wasn't tilt-corrected. The library
never loads the page scan itself — take imageUrl and load it with your renderer
(see Quick Start).
Words#
H.words: Word[] // sorted by (lineIdx, x0)
H.lines: Word[][] // the same words grouped into lines
H.wordById(id: number): Word | undefined
H.bboxOf(words: Iterable<Word>): Bbox // union bbox; zero-area at origin if empty
Word#
interface Word {
id: number; // DB primary key — stable across re-OCRs
text: string;
x0: number; y0: number; x1: number; y1: number; // pixel bbox
lineIdx: number; // 0-based line index within the page
conf: number; // OCR confidence in [0, 1]
prefix: string | null; // leading punctuation/quotes, else null
suffix: string | null; // trailing punctuation, else null
pos: string | null; // spaCy UPOS tag — NOUN, VERB, ADJ, …
lemma: string | null;
freq: number | null; // wordfreq score (modern-corpus frequency)
rarity: number | null; // normalized log inverse frequency, [0,1]; higher = rarer
isContent: 0 | 1; // 1 if NOUN/VERB/ADJ/ADV
isConnective: 0 | 1; // 1 if it reads as a connective bridge
}
rarity and isContent are what the chunk scorer
rewards; pos drives the chunker's patterns.
Bbox and Pt#
interface Bbox { x0: number; y0: number; x1: number; y1: number }
interface Pt { x: number; y: number }
bboxOf is the usual bridge from a phrase to a balloon:
Whitespace geometry#
The pipeline precomputes these so runtime needs no image analysis. See Concepts → Whitespace geometry for the narrative; the shapes:
H.gutters: Gutter[]#
interface Gutter {
gutterId: number;
kind: 'h_line' | 'v_slit'; // horizontal inter-line gap / vertical intra-line slit
lineIdxA: number | null; // line above (h_line) or left (v_slit); null at edges
lineIdxB: number | null; // line below (h_line) or right (v_slit); null at edges
x0: number; y0: number; x1: number; y1: number;
polyline: [number, number][]; // centre-line of the channel
minWidth: number; // narrowest width along its length, px
riverScore: number; // heuristic: how "river-like" it looks
}
H.docks: Map<number, Dock>#
Keyed by wordId.
interface Dock {
wordId: number;
breathingTop: number; breathingBottom: number; // px to nearest obstacle
breathingLeft: number; breathingRight: number; // in each direction
slackDirection: string; // direction with most slack, e.g. "NE"
ports: Port[]; // entry/exit points into gutters
dockAbove: number | null; dockBelow: number | null;
dockLeft: number | null; dockRight: number | null; // neighbouring gutter ids
}
interface Port {
x: number; y: number;
gutterId: number;
compass: 'N' | 'S' | 'E' | 'W';
nodeId: number; // corresponding node in H.graph
}
H.graph: PageGraph#
interface PageGraph { nodes: Map<number, GraphNode> }
interface GraphNode {
id: number;
x: number; y: number;
kind: string; // informational (empty in the JSON export)
edges: [number, number, number][]; // [neighborId, edgeCost, gutterId] tuples
}
This is the graph the Dijkstra pathfinder walks. You rarely touch it directly —
H.river.between does — but H.river.dijkstra
and H.river.penalizeBorders operate on it if you want to.