import { z } from 'zod'; /** * Search schemas and views (issue #49/#50, ADR 0010). Full-text search runs on * PostgreSQL behind the `SearchProvider` interface. Diacritic-insensitive * matching ('Baume' finds 'Bäume') is achieved by normalizing both the indexed * text and the query in application code — so no Postgres `unaccent` extension * is required (which keeps the schema-pushed test databases working). */ // Combining diacritical marks (U+0300–U+036F), removed after NFKD decomposition. const COMBINING_MARKS = /[̀-ͯ]/g; /** * Fold a string for search: strip diacritics (NFKD + drop combining marks) and * lowercase, leaving tokenization to Postgres `to_tsvector('simple', …)`. Used * for BOTH the stored search vector and the query, so they always agree. */ export function normalizeForSearch(text: string): string { return text.normalize('NFKD').replace(COMBINING_MARKS, '').toLowerCase(); } /** Sentinels wrapping a matched span in a result snippet (`ts_headline`), split * by the UI to render highlights without trusting HTML from the content. * Private-use codepoints that never occur in page text. */ export const SEARCH_HIGHLIGHT_START = String.fromCodePoint(0xe000); export const SEARCH_HIGHLIGHT_END = String.fromCodePoint(0xe001); /** How many results one search returns (v1, ADR 0010 target scale). */ export const SEARCH_RESULT_LIMIT = 30; export const searchQuerySchema = z.object({ q: z.string().trim().min(1, 'validation.required').max(200, 'validation.tooLong'), /** Restrict to one pond; omitted = all ponds the user may read. */ pondId: z.string().min(1).optional(), /** Restrict to pages carrying any of these label ids. */ labels: z.array(z.string().min(1)).optional(), }); export type SearchQuery = z.infer; /** One hit: the page, its pond, its labels, and a highlighted snippet. */ export interface SearchResultView { pageId: string; title: string; slug: string; pondId: string; pondSlug: string; pondName: string; labelIds: string[]; /** Snippet with matches wrapped in the highlight sentinels above. */ snippet: string; }