# domain/converters/search.converter.ts

> 119 lines of code and 19 definitions.

Tree: Site tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/tree#file-domain-converters-search-converter-ts
Source text: https://banes-lab.com/assets/sources/source.c6de9cfe7ddebfb4c5af7e44004ec33d574727b1474f1b9aba4c682781a0ba7d.generated.txt

## Definitions

- `byPlace` (lexical_declaration, line 75)
- `definitionHits` (lexical_declaration, line 79)
- `definitionSection` (lexical_declaration, line 59)
- `sectionHits` (lexical_declaration, line 37)
- `definitionMatches` (lexical_declaration, line 51, exported)
- `spreadsOf` (lexical_declaration, line 110, exported)
- `searchSpreads` (lexical_declaration, line 123, exported)
- `compareHits` (lexical_declaration, line 102)
- `ID_JOIN` (lexical_declaration, line 19)
- `OpenSpread` (interface_declaration, line 21)
- `DEFINITION_SOURCE` (lexical_declaration, line 27)
- `narrowed` (lexical_declaration, line 40)
- `home` (lexical_declaration, line 44)
- `position` (lexical_declaration, line 45)
- `name` (lexical_declaration, line 55, exported)
- `spreads` (lexical_declaration, line 111, exported)
- `last` (lexical_declaration, line 113, exported)
- `terms` (lexical_declaration, line 124, exported)
- `index` (lexical_declaration, line 128, exported)

## Uses

- [domain/converters/source.converter.ts](https://banes-lab.com/source/tree/domain/converters/source.converter.ts.md)

## Source

```typescript
import { ANATOMY_TITLE, TITLE_SEPARATOR } from "#configuration/strings/page.strings";
import { DEFINITION_MIN_TERM_LENGTH, DEFINITION_RESULT_LIMIT } from "#configuration/constants/search.constants";
import type { SearchCorpus, SearchHit, SearchIndex, SearchSource, SearchSpread } from "#types/search.types";
import { UNPLACED, anchorPath } from "#core/analyzers/search.analyzer";
import { definitionHref, pathLabel } from "#domain/converters/source.converter";
import { indexOf, positionOf } from "#domain/converters/search.index.converter";
import { ANATOMY_ICON } from "#configuration/icons/page.icons";
import { ANATOMY_PAGE } from "#core/ids/page.ids";
import { ANATOMY_TONE } from "#configuration/constants/tab.constants";
import type { DefinitionLocation } from "#types/anatomy.types";
import { SEARCH_DEFINITIONS_ICON } from "#configuration/icons/search.icons";
import { SEARCH_DEFINITIONS_SECTION_ID } from "#core/ids/search.ids";
import { SEARCH_DEFINITIONS_TITLE } from "#configuration/strings/search.strings";
import type { Section } from "#types/document.types";
import { TREE_TAB } from "#core/ids/anatomy.ids";
import { narrowSection } from "#domain/converters/search.fragment.converter";
import { queryTerms } from "#core/matchers/search.matcher";

const ID_JOIN = "-";

interface OpenSpread {
    readonly home: string;
    readonly sections: Section[];
    readonly source: SearchSource;
}

const DEFINITION_SOURCE: SearchSource = {
    first: false,
    icon: ANATOMY_ICON,
    label: ANATOMY_TITLE + TITLE_SEPARATOR + SEARCH_DEFINITIONS_TITLE,
    layout: "chapter",
    page: ANATOMY_PAGE,
    tab: { icon: ANATOMY_ICON, id: TREE_TAB, label: SEARCH_DEFINITIONS_TITLE, sections: [] },
    tone: ANATOMY_TONE,
};

const sectionHits = function sectionHits(index: SearchIndex, terms: readonly string[]): readonly SearchHit[] {
    return index.sources.flatMap((source, sourceIndex) =>
        source.tab.sections.flatMap((section, sectionIndex) => {
            const narrowed = narrowSection(terms, section);
            if (narrowed === null) {
                return [];
            }
            const home = anchorPath(source, section.id);
            const position = positionOf(index, source, narrowed);
            return [{ home, position, section: narrowed, sectionIndex, source, sourceIndex }];
        }),
    );
};

export const definitionMatches = function definitionMatches(
    terms: readonly string[],
    location: DefinitionLocation,
): boolean {
    const name = location.name.toLowerCase();
    return terms.every((term) => term.length >= DEFINITION_MIN_TERM_LENGTH && name.includes(term));
};

const definitionSection = function definitionSection(location: DefinitionLocation, at: number): Section {
    return {
        icon: SEARCH_DEFINITIONS_ICON,
        id: SEARCH_DEFINITIONS_SECTION_ID + ID_JOIN + String(at),
        subsections: [
            {
                blocks: [
                    { file: location.file, kind: "definition", name: location.name, title: pathLabel(location.file) },
                ],
                title: pathLabel(location.file),
            },
        ],
        title: location.name,
    };
};

const byPlace = function byPlace(left: number, right: number): number {
    return left === right ? 0 : left - right;
};

const definitionHits = function definitionHits(
    index: SearchIndex,
    corpus: SearchCorpus,
    terms: readonly string[],
): readonly SearchHit[] {
    return corpus.definitions
        .filter((location) => definitionMatches(terms, location))
        .map((location) => ({ location, position: index.evidence.get(location.name) ?? UNPLACED }))
        .toSorted(
            (left, right) =>
                byPlace(left.position, right.position) || left.location.name.localeCompare(right.location.name),
        )
        .slice(0, DEFINITION_RESULT_LIMIT)
        .map(({ location, position }, at) => ({
            home: definitionHref(location.name, location.file),
            position,
            section: definitionSection(location, at),
            sectionIndex: at,
            source: DEFINITION_SOURCE,
            sourceIndex: index.sources.length,
        }));
};

const compareHits = function compareHits(left: SearchHit, right: SearchHit): number {
    return (
        byPlace(left.position, right.position) ||
        left.sourceIndex - right.sourceIndex ||
        left.sectionIndex - right.sectionIndex
    );
};

export const spreadsOf = function spreadsOf(hits: readonly SearchHit[]): readonly SearchSpread[] {
    const spreads: OpenSpread[] = [];
    for (const hit of hits) {
        const last = spreads.at(-1);
        if (last?.source === hit.source) {
            last.sections.push(hit.section);
            continue;
        }
        spreads.push({ home: hit.home, sections: [hit.section], source: hit.source });
    }
    return spreads;
};

export const searchSpreads = function searchSpreads(corpus: SearchCorpus, query: string): readonly SearchSpread[] {
    const terms = queryTerms(query);
    if (terms.length === 0) {
        return [];
    }
    const index = indexOf(corpus);
    return spreadsOf([...sectionHits(index, terms), ...definitionHits(index, corpus, terms)].toSorted(compareHits));
};
```
