# labels - which cells could be the value a label announces Source: https://truecopy.dev/docs/labels/ Module: truecopy/labels A label and its figure are scattered by the layout, not by the domain. Three applications wrote the same search three times, in three geometries, with no domain word in any of them. ```ts import { labelledValues, columnOfHeader } from 'truecopy/labels'; const found = labelledValues(rows, isLabel, isValue, options?); // [{ label: { row, column }, values: [{ row, column, raw, distance }, …] }, …] ``` [`contract`](https://truecopy.dev/docs/contract/) states the need in as many words and answered none of it: `SelfCheck.declared` is a **list** because a document may announce several candidate values when its layout scattered a label from its number. Three applications wrote that search separately - four lines under a heading, the first amount to its right, a column index read out of a header cell. Same question, three geometries, and no word about banking, property or pensions in any of them. ## It will not pick `values` comes back **closest first**, and empty when the document offers none, which is an answer of its own: a label with no value beside it is worth saying out loud. Handing back one value would decide between two readings of a layout. The list feeds `SelfCheck.declared` and [`readDocument`](https://truecopy.dev/docs/contract/) keeps the one that fits, so the **document** decides rather than a rule about how far a number usually sits from its heading. `isLabel` and `isValue` are yours, and that is the whole line this library holds: it does not know a total from a heading from a footnote, and a list of words meaning "total" would be a domain shipped inside a parser. ```ts labelledValues( rows, (cell) => accentFree(cell).startsWith('total'), (cell) => isOnlyNumber(cell, 2) ); ``` ## A search stops at the next label One rule costs a reading when it is missing, and it was met on a real pension record: two headings printed close together let the second one's figure count for the first as well. A doubled total, a false proof, and a refusal on a reading that was right. | Option | | | ------- | -------------------------------------------------------------------- | | `reach` | how many cells to walk. Four by default, measured rather than chosen | | `look` | `'row'`, `'column'` or `'both'`. Both by default | Four clears the presentation prose and the form reference a pension record prints between a heading and its figure, without reaching the next section. `Look` says where: along the label's own row to the right (`Total 1 234,56`), down its own column (a header, and its figures under it), or both merged and sorted by distance. A caller who knows its layout says so; one that does not gets every `Candidate` with its `distance`. `distance` is counted in **cells**, never in points. A distance in points would say a wide column is farther than a narrow one, which is a fact about the typesetting and not about which value belongs to which heading. `raw` is the cell exactly as the document prints it, never parsed. Reading it is your call, and it is one line: `readNumber(raw, decimalMarkOf(document.text))`. ## The unit announced once, in the header ```ts const surface = columnOfHeader(rows, (cell) => accentFree(cell).includes('(en m2)')); ``` A property schedule announces its unit once, in the header, and prints bare numbers underneath. Requiring the unit inside each cell returned no value at all on a real corpus; reading it from the header returned them. `null` when no header matches, and **null again when several do**. Two matching headers is exactly the document nobody should read on a hunch: either the predicate is too loose or the table carries two of that column, and picking the first would be a silent answer to a question nobody asked. ## The same question on prose, with no grid `labelledValues` needs rows and columns. A caller holding an API field, an OCR pass or a text-layer dump has neither, and the question does not go away with the geometry: it was written a fourth time, by a fourth caller, and that caller got it wrong. ```ts import { labelledSpans } from 'truecopy/labels'; const found = labelledSpans( notice, /exercice clos le \d{1,2} \p{L}+ \d{4}/giu, /\d{1,4},\d{2} euros/g ); // [{ label: { index, raw }, values: [{ index, raw, distance }, …] }, …] ``` Same contract as its sibling, minus a dimension. You say what a label looks like and what a value looks like, the walk stops at the **next label**, and it hands back candidates rather than picking one. Each `Labelling` pairs one label with its candidates, exactly as `Labelled` does on a grid. A `Span` is an index and the `raw` text at it; a `TextCandidate` adds `distance`, counted in **characters** between the end of the label and the start of the value, zero meaning they touch. `SpanOptions` carries one: | Option | | | ------- | ------------------------------------------------------------------ | | `reach` | how many characters to walk past a label. **Unbounded by default** | Unbounded is not laziness: in text the next label _is_ the boundary, and a character budget would be a number nobody measured. The one case it guards is the last label of a document, which otherwise reaches to the end. A caller who knows its own layout narrows it. ## A value inside its own label is not a candidate The year in `exercice clos le 31 decembre 2024` is part of the label, not the figure that label announces. Returning it would answer a question nobody asked, so an overlapping match is dropped. What this was written against is one step further out, and it published a whole series wrong: a French dividend table prints the **payment date** between each exercice and its amount, so a caller walking from year to year let the payment year collect the amount. Every value in the series was well formed, and every one of them sat under the wrong year. Map of this site for a model: https://truecopy.dev/llms.txt Every page in one file: https://truecopy.dev/llms-full.txt