From 517be2062ed96033efa487cfd1e53a381dbb6bfa Mon Sep 17 00:00:00 2001 From: Wessel Verheij Date: Sun, 13 Sep 2026 16:37:53 +0200 Subject: [PATCH] docs(b9): say that search folds case over all of Unicode, at every length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B9 described two matching arms -- the index for a query long enough to tokenise, a bounded scan for one that is not -- and said nothing about how either compares. Measured in the product, they did not compare alike: the index folds case over the whole of Unicode and the fallback folded ASCII, so a reader typing their merchant's name in the case they see it found the row at three characters and not at two. That is reader-visible behaviour with no requirement over it, so B9-R19 states it: one fold, Unicode-wide, the same in every arm, and case only -- folding case is not stripping accents, and `o` still does not find `ö`. Signed-off-by: Wessel Verheij --- 10-functional/features/b-ledger/b9-search.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/10-functional/features/b-ledger/b9-search.md b/10-functional/features/b-ledger/b9-search.md index 7a25273..a3d1905 100644 --- a/10-functional/features/b-ledger/b9-search.md +++ b/10-functional/features/b-ledger/b9-search.md @@ -35,6 +35,9 @@ indexing. - A text query long enough to tokenise uses the index. A shorter one falls back to a **bounded** scan that decrypts and substring-matches, so short queries still work without an unbounded table walk. +- Matching ignores case over the whole of Unicode, and by the same rule in both + arms, so the length of a word never changes which rows it reaches. Accents are + part of the letter: `o` does not find `ö`, but `ö` finds `Ö`. - Typed tokens narrow by account, category, amount, and date bounds. The same filters are available as controls, so the tokens are a shortcut rather than a requirement. @@ -71,7 +74,8 @@ with token autocompletion and recent searches. | Situation | Behaviour | |-----------|-----------| -| A query too short to tokenise | Bounded decrypt-and-scan fallback. | +| A query too short to tokenise | Bounded decrypt-and-scan fallback, folding case the same way the index does. | +| A merchant name the reader types in a case the statement does not use | Found, accents included, at every query length. | | Zero results | A single spelling suggestion, where the query is long enough to make one meaningful. | | A merchant name containing markup characters | Escaped before highlighting; no injection. | | An index write failure during import | The import chunk rolls back. Loud, not silent. | @@ -103,6 +107,7 @@ with token autocompletion and recent searches. | **B9-R15** | Amount matching MUST read the stored minor-unit amounts directly. | | **B9-R16** | The command palette MUST be backed by the same search service and MUST separate transaction and entity results. | | **B9-R17** | Every search MUST be scoped to the requesting user. | +| **B9-R19** | Case-insensitive matching MUST fold over the whole of Unicode rather than ASCII alone, and MUST apply the same fold in every search arm, so that a query's length never changes which rows a word reaches. Folding case MUST NOT strip diacritics. | ## Related