(function () {
    "use strict";

    self.window = self;

    const MAX_RESULTS = 50;
    const RELATED_TAG_LIMIT = 12;
    const TAG_RENAMES = new Map([
        ["hair_twirling", "twirling_hair"],
        ["hair twirling", "twirling_hair"],
        ["vagina", "pussy"]
    ]);
    const COLOR_COMPONENTS = new Set(["red", "blue", "green", "black", "white", "brown", "blonde", "yellow", "purple", "pink", "orange", "aqua", "grey", "gray", "silver", "gold"]);
    const RELATED_WORD_STOPWORDS = new Set(["the", "and", "from", "with", "for", "into", "non", "of", "to", "in", "on", "as", "or", "a", "an", "is", "are", "this", "that"]);
    const HAIR_CONTEXT_PENALTY_COMPONENTS = new Set(["another's", "arm", "armpit", "ass", "body", "breast", "chest", "cum", "facial", "grab", "grabbing", "hairjob", "hold", "holding", "leg", "navel", "nipple", "penis", "pull", "pulling", "pubic", "pussy", "sex", "stray"]);
    const FAMILY_OBJECT_COMPONENTS = new Set(["hair"]);
    const FAMILY_REQUIRED_CONTEXT_COMPONENTS = new Set(["grab", "grabbing", "hold", "holding", "wet"]);
    const DIRECT_COMPONENT_ALIASES = [
        { terms: ["髪", "髪の毛", "ヘア", "へあ"], components: ["hair"] },
        { terms: ["つかむ", "つかみ", "つかん", "つかま", "つかめ", "つかっ", "掴む", "掴み", "掴ん", "掴ま", "掴め", "掴っ"], components: ["grab", "grabbing"] },
        { terms: ["握る", "握り", "握っ", "握ら", "握れ"], components: ["grab", "grabbing", "hold", "holding"] },
        { terms: ["持つ", "持ち", "持っ", "持た", "持て"], components: ["hold", "holding"] },
        { terms: ["座る", "座り", "座っ", "座ら", "座れ"], components: ["sitting"] },
        { terms: ["立つ", "立ち", "立っ", "立た", "立て"], components: ["standing"] },
        { terms: ["濡れる", "濡れ", "濡ら"], components: ["wet"] }
    ];
    const JAPANESE_VERB_READING_VARIANTS = [
        ["摑", "つか"], ["掴", "つか"], ["揉", "も"], ["撫で", "なて"], ["なで", "なて"], ["撫ぜ", "なぜ"],
        ["咥え", "くわえ"], ["銜え", "くわえ"], ["舐め", "なめ"], ["挟ま", "はさま"], ["挟み", "はさみ"],
        ["挟む", "はさむ"], ["挟ん", "はさん"], ["縛ら", "しばら"], ["縛り", "しばり"], ["縛る", "しばる"],
        ["縛っ", "しばっ"], ["繋が", "つなが"], ["繋ぎ", "つなぎ"], ["繋ぐ", "つなぐ"], ["繋げ", "つなげ"],
        ["繫が", "つなが"], ["繫ぎ", "つなぎ"], ["繫ぐ", "つなぐ"], ["繫げ", "つなげ"], ["結ば", "むすば"],
        ["結び", "むすび"], ["結ぶ", "むすぶ"], ["結ん", "むすん"], ["触ら", "さわら"], ["触り", "さわり"],
        ["触る", "さわる"], ["触れ", "ふれ"], ["開け", "あけ"], ["開い", "あい"], ["閉じ", "とじ"],
        ["閉め", "しめ"], ["広げ", "ひろげ"], ["拡げ", "ひろげ"], ["曲げ", "まげ"], ["伸ば", "のば"],
        ["隠し", "かくし"], ["隠す", "かくす"], ["脱が", "ぬが"], ["脱ぎ", "ぬぎ"], ["脱ぐ", "ぬぐ"],
        ["着る", "きる"], ["着て", "きて"], ["座ら", "すわら"], ["座り", "すわり"], ["座る", "すわる"],
        ["立た", "たた"], ["立ち", "たち"], ["立つ", "たつ"], ["寝る", "ねる"], ["寝て", "ねて"],
        ["持た", "もた"], ["持ち", "もち"], ["持つ", "もつ"], ["乗ら", "のら"], ["乗り", "のり"],
        ["乗る", "のる"], ["見せ", "みせ"], ["見る", "みる"], ["笑う", "わらう"], ["笑い", "わらい"],
        ["泣く", "なく"], ["泣き", "なき"]
    ];

    let DATA = [];
    let QUERY_LEXICON = {};
    let LEXICON_TERMS = [];
    const TAG_INDEX = new Map();
    const TAG_EXACT_INDEX = new Map();
    const SEARCH_DATA = [];
    const SEARCH_DATA_BY_KEY = new Map();

    function canonicalTagName(tag) {
        const text = String(tag || "").trim();
        const key = text.toLowerCase().replaceAll(" ", "_");
        return TAG_RENAMES.get(key) || text;
    }

    function splitAliases(value) {
        return String(value || "").split(/[,、，\r\n]+/).map((alias) => alias.trim()).filter(Boolean);
    }

    function uniqueTextList(items) {
        const seen = new Set();
        const result = [];
        for (const item of items) {
            const text = String(item || "").trim();
            if (!text) {
                continue;
            }
            const key = text.toLowerCase();
            if (seen.has(key)) {
                continue;
            }
            seen.add(key);
            result.push(text);
        }
        return result;
    }

    function mergeAliasText() {
        return uniqueTextList(Array.from(arguments).flatMap((value) => splitAliases(value))).join(",");
    }

    function numberValue(value) {
        const number = Number(value || 0);
        return Number.isFinite(number) && number > 0 ? number : 0;
    }

    function canonicalKey(value) {
        return String(value || "").trim().toLowerCase().replaceAll(" ", "_");
    }

    function debugEditKey(value) {
        return canonicalKey(canonicalTagName(value));
    }

    function normalizePlaceholderDisplay(value) {
        return String(value || "")
            .trim()
            .replace(/[○〇◯]{2,}|[～〜]+/gu, "〇〇")
            .replace(/([A-Za-z0-9])〇〇/gu, "$1 〇〇")
            .replace(/〇〇([A-Za-z0-9])/gu, "〇〇 $1")
            .replace(/\s+/g, " ")
            .trim();
    }

    function normalizePlaceholderText(value) {
        return String(value || "")
            .trim()
            .replace(/[○〇◯]{2,}/gu, "〇〇")
            .replace(/([A-Za-z][A-Za-z0-9_.'()/-]*)[～〜]+/gu, "$1 〇〇")
            .replace(/[～〜]+([A-Za-z][A-Za-z0-9_.'()/-]*)/gu, "〇〇 $1")
            .replace(/([A-Za-z][A-Za-z0-9_.'()/-]*)〇〇/gu, "$1 〇〇")
            .replace(/〇〇([A-Za-z][A-Za-z0-9_.'()/-]*)/gu, "〇〇 $1")
            .replace(/\s+/g, " ")
            .trim();
    }

    function normalizePlaceholderAliasText(value) {
        return uniqueTextList(splitAliases(value).map((alias) => normalizePlaceholderText(alias))).join(",");
    }

    function dictionarySectionSet(value) {
        return new Set(String(value || "").replaceAll("／", "/").split("/").map((section) => section.trim()).filter(Boolean));
    }

    function leadingDictionaryLabel(description) {
        const text = String(description || "").trim();
        const match = text.match(/^([ぁ-んァ-ヶ一-龠A-Za-z0-9・ー〇〇]{1,24}?)(?:な雰囲気に|風に|になる|が出る|が描写される|。|、|…)/u);
        if (!match) {
            return "";
        }
        const label = String(match[1] || "").replace(/[「」『』（）()\s]+/g, "").trim();
        if (!label || label.length > 20 || /(少し|全体|場合|こちら|それ|これ|ため|よう)/u.test(label)) {
            return "";
        }
        return label;
    }

    function normalizePlaceholderEntry(entry) {
        entry.display_tag = normalizePlaceholderDisplay(entry.display_tag);
        entry.edit_display_tag = normalizePlaceholderDisplay(entry.edit_display_tag);
        entry.description = normalizePlaceholderText(entry.description);
        entry.edit_description = normalizePlaceholderText(entry.edit_description);
        entry.aliases = normalizePlaceholderAliasText(entry.aliases);
        entry.search_aliases = normalizePlaceholderAliasText(entry.search_aliases);
        if (String(entry.is_dictionary || "") === "1" && !String(entry.edit_ja || "").trim()) {
            const ja = String(entry.ja || "").trim();
            if (ja && dictionarySectionSet(entry.dictionary_section).has(ja)) {
                const label = leadingDictionaryLabel(entry.description);
                if (label) {
                    entry.ja = label;
                    entry.aliases = mergeAliasText(entry.aliases, label);
                }
            }
        }
        return entry;
    }

    function mergeDictionaryEntry(primary, secondary) {
        primary.ja = primary.ja || secondary.ja || "";
        primary.display_tag = primary.display_tag || secondary.display_tag || "";
        primary.description = primary.description || secondary.description || "";
        primary.aliases = mergeAliasText(primary.aliases, secondary.aliases, secondary.tag);
        primary.search_aliases = mergeAliasText(primary.search_aliases, secondary.search_aliases);
        primary.related_tags = mergeAliasText(primary.related_tags, secondary.related_tags);
        primary.dictionary_section = primary.dictionary_section || secondary.dictionary_section || "";
        primary.post_count = String(Math.max(numberValue(primary.post_count), numberValue(secondary.post_count)));
        primary.rank_count = String(Math.max(numberValue(primary.rank_count), numberValue(secondary.rank_count)));
        primary.category = primary.category || secondary.category || "";
        primary.is_deprecated = String(primary.is_deprecated || "") === "1" || String(secondary.is_deprecated || "") === "1" ? "1" : "0";
        primary.has_tag = String(primary.has_tag || "") === "1" || String(secondary.has_tag || "") === "1" ? "1" : "0";
        primary.has_wiki = String(primary.has_wiki || "") === "1" || String(secondary.has_wiki || "") === "1" ? "1" : "0";
        primary.is_dictionary = String(primary.is_dictionary || "") === "1" || String(secondary.is_dictionary || "") === "1" ? "1" : "0";
        primary.importance = String(Math.max(numberValue(primary.importance), numberValue(secondary.importance)));
        return primary;
    }

    function normalizeDictionaryData(rawData) {
        const mergedByTag = new Map();
        const order = [];
        for (const rawEntry of rawData || []) {
            const rawTag = rawEntry && (rawEntry.tag || rawEntry["\ufefftag"] || rawEntry["ï»¿tag"] || rawEntry["﻿tag"]);
            const entry = { ...rawEntry, tag: rawTag || "" };
            if (!entry.tag) {
                continue;
            }
            const canonicalTag = canonicalTagName(entry.tag);
            if (canonicalTag !== entry.tag) {
                entry.aliases = mergeAliasText(entry.aliases, entry.tag, String(entry.tag || "").replaceAll("_", " "));
                entry.tag = canonicalTag;
            }
            normalizePlaceholderEntry(entry);
            const existing = mergedByTag.get(canonicalTag);
            if (!existing) {
                mergedByTag.set(canonicalTag, entry);
                order.push(canonicalTag);
                continue;
            }
            const preferNew = String(entry.is_dictionary || "") === "1" && String(existing.is_dictionary || "") !== "1";
            const primary = preferNew ? entry : existing;
            const secondary = preferNew ? existing : entry;
            mergedByTag.set(canonicalTag, mergeDictionaryEntry(primary, secondary));
        }
        return order.map((tag) => mergedByTag.get(tag)).filter(Boolean);
    }

    function displayTagValue(entry) {
        return String(entry.edit_display_tag || entry.display_tag || entry.tag || "").trim();
    }

    function jaValue(entry) {
        return String(entry.edit_ja || entry.ja || "").trim();
    }

    function descriptionValue(entry) {
        return String(entry.edit_description || entry.description || "").trim();
    }

    function relatedTagsValue(entry) {
        return String(entry.edit_related_tags || entry.related_tags || "").trim();
    }

    function relatedTagsAreOverridden(entry) {
        return String(entry.edit_related_tags_override || "") === "1";
    }

    function applyRowToEntry(entry, row) {
        for (const key of [
            "tag", "display_tag", "ja", "description", "aliases", "search_aliases", "related_tags", "dictionary_section",
            "post_count", "rank_count", "category", "is_deprecated", "has_tag", "has_wiki", "wiki_note", "is_dictionary", "importance",
            "edit_display_tag", "edit_ja", "edit_description", "edit_related_tags", "edit_related_tags_override", "edit_promoted", "edit_updated_at", "edit_removed"
        ]) {
            if (Object.prototype.hasOwnProperty.call(row, key)) {
                entry[key] = String(row[key] || "");
            }
        }
        normalizePlaceholderEntry(entry);
    }

    function applyEditOverlayToEntry(entry, edit) {
        for (const key of ["display_tag", "ja", "description", "related_tags", "edit_display_tag", "edit_ja", "edit_description", "edit_related_tags", "edit_related_tags_override", "edit_promoted", "edit_updated_at"]) {
            if (Object.prototype.hasOwnProperty.call(edit, key) && String(edit[key] || "").trim()) {
                entry[key] = String(edit[key] || "");
            }
        }
        normalizePlaceholderEntry(entry);
    }

    function applyDictionaryEdits(data, rawEdits) {
        const byTag = new Map();
        for (const entry of data) {
            byTag.set(debugEditKey(entry.tag), entry);
        }
        for (const rawEdit of rawEdits || []) {
            const edit = normalizePlaceholderEntry({ ...rawEdit });
            const tag = canonicalTagName(edit.tag);
            if (!tag) {
                continue;
            }
            edit.tag = tag;
            const key = debugEditKey(tag);
            const existing = byTag.get(key);
            if (String(edit.edit_removed || "") === "1") {
                if (existing) {
                    const index = data.indexOf(existing);
                    if (index >= 0) {
                        data.splice(index, 1);
                    }
                    byTag.delete(key);
                }
                continue;
            }
            if (existing) {
                applyEditOverlayToEntry(existing, edit);
            } else {
                data.push(edit);
                byTag.set(key, edit);
            }
        }
        return data;
    }

    function katakanaToHiragana(value) {
        return value.replace(/[\u30a1-\u30f6]/g, (char) => String.fromCharCode(char.charCodeAt(0) - 0x60));
    }

    function normalizeJapaneseVerbReadings(value) {
        let text = value;
        for (const [source, target] of JAPANESE_VERB_READING_VARIANTS) {
            text = text.replaceAll(source, target);
        }
        return text;
    }

    function normalize(value) {
        let text = katakanaToHiragana(String(value || "").normalize("NFKC").toLowerCase()).replaceAll("髮", "髪");
        const colorAliases = new Map([
            ["赤い", "赤"], ["赤色の", "赤"], ["青い", "青"], ["青色の", "青"], ["黒い", "黒"], ["黒色の", "黒"],
            ["白い", "白"], ["白色の", "白"], ["黄色い", "黄"], ["黄色の", "黄"], ["茶色い", "茶"], ["茶色の", "茶"],
            ["緑色の", "緑"], ["紫色の", "紫"], ["灰色の", "灰"], ["金色の", "金"], ["銀色の", "銀"],
            ["ぴんく色の", "ぴんく"], ["水色の", "水色"]
        ]);
        for (const [source, target] of colorAliases) {
            text = text.replaceAll(source, target);
        }
        text = normalizeJapaneseVerbReadings(text);
        if (text === "へあ" || text === "へあー") {
            text = "髪";
        }
        return text.replaceAll("の毛", "").replaceAll("のけ", "").replace(/[をがにのでへ\s\p{P}\p{S}]/gu, "");
    }

    function normalizeComponentAlias(value) {
        return normalizeJapaneseVerbReadings(katakanaToHiragana(String(value || "").normalize("NFKC").toLowerCase()).replaceAll("髮", "髪")).replace(/[\s\p{P}\p{S}]/gu, "");
    }

    function normalizeSymbol(value) {
        return String(value || "").normalize("NFKC").toLowerCase().trim().replaceAll("_", " ").replace(/\s+/g, " ");
    }

    function canonicalSearchTagKey(value) {
        const key = canonicalKey(normalizeSymbol(value));
        return TAG_RENAMES.get(key) || key;
    }

    function tagComponents(value) {
        const seen = new Set();
        const components = [];
        for (const match of String(value || "").toLowerCase().replaceAll("_", " ").replaceAll("-", " ").matchAll(/[a-z0-9][a-z0-9']*/g)) {
            const component = match[0];
            if (component.length < 2 || ["the", "and", "from", "with", "for", "into", "non"].includes(component) || seen.has(component)) {
                continue;
            }
            seen.add(component);
            components.push(component);
        }
        return components;
    }

    function buildLexiconTerms(lexicon) {
        return Object.entries(lexicon || {})
            .map(([term, components]) => ({ term: normalize(term), components: Array.isArray(components) ? components : [] }))
            .filter((item) => item.term && item.components.length)
            .sort((left, right) => right.term.length - left.term.length);
    }

    function indexKeysForEntry(entry) {
        const displayTag = displayTagValue(entry);
        return uniqueTextList([entry.tag, String(entry.tag || "").replaceAll("_", " "), String(entry.tag || "").replaceAll(" ", "_"), displayTag, String(displayTag || "").replaceAll(" ", "_")]).map((value) => canonicalKey(value)).filter(Boolean);
    }

    function indexEntry(entry) {
        if (Array.isArray(entry._pdIndexKeys)) {
            for (const key of entry._pdIndexKeys) {
                if (TAG_INDEX.get(key) === entry) {
                    TAG_INDEX.delete(key);
                }
            }
        }
        const keys = indexKeysForEntry(entry);
        for (const key of keys) {
            TAG_INDEX.set(key, entry);
        }
        entry._pdIndexKeys = keys;
    }

    function buildSearchRecord(entry) {
        const aliases = splitAliases(entry.aliases);
        const searchAliases = aliases.concat(splitAliases(entry.search_aliases));
        return {
            entry,
            components: tagComponents(entry.tag),
            tag: normalize(entry.tag),
            tagSymbol: normalizeSymbol(entry.tag),
            displayTag: normalize(displayTagValue(entry)),
            displayTagSymbol: normalizeSymbol(displayTagValue(entry)),
            ja: normalize(jaValue(entry)),
            jaSymbol: normalizeSymbol(jaValue(entry)),
            description: normalize(descriptionValue(entry)),
            descriptionSymbol: normalizeSymbol(descriptionValue(entry)),
            aliases: searchAliases.map((alias) => ({ loose: normalize(alias), symbol: normalizeSymbol(alias) })).filter((alias) => alias.loose || alias.symbol),
            popularity: tagPopularity(entry)
        };
    }

    function rebuildSearchIndexes() {
        TAG_INDEX.clear();
        TAG_EXACT_INDEX.clear();
        SEARCH_DATA.length = 0;
        SEARCH_DATA_BY_KEY.clear();
        for (const entry of DATA) {
            TAG_EXACT_INDEX.set(debugEditKey(entry.tag), entry);
            indexEntry(entry);
        }
        for (const entry of DATA) {
            const record = buildSearchRecord(entry);
            SEARCH_DATA.push(record);
            SEARCH_DATA_BY_KEY.set(debugEditKey(entry.tag), record);
        }
    }

    function refreshSearchIndexEntry(entry) {
        indexEntry(entry);
        const key = debugEditKey(entry.tag);
        TAG_EXACT_INDEX.set(key, entry);
        const previous = SEARCH_DATA_BY_KEY.get(key);
        const record = buildSearchRecord(entry);
        if (previous) {
            const index = SEARCH_DATA.indexOf(previous);
            if (index >= 0) {
                SEARCH_DATA[index] = record;
            } else {
                SEARCH_DATA.push(record);
            }
        } else {
            SEARCH_DATA.push(record);
        }
        SEARCH_DATA_BY_KEY.set(key, record);
    }

    function removeSearchIndexEntry(tag) {
        const key = debugEditKey(tag);
        TAG_EXACT_INDEX.delete(key);
        const record = SEARCH_DATA_BY_KEY.get(key);
        if (record) {
            const index = SEARCH_DATA.indexOf(record);
            if (index >= 0) {
                SEARCH_DATA.splice(index, 1);
            }
            SEARCH_DATA_BY_KEY.delete(key);
        }
        DATA = DATA.filter((entry) => debugEditKey(entry.tag) !== key);
        for (const [indexKey, entry] of Array.from(TAG_INDEX.entries())) {
            if (debugEditKey(entry.tag) === key) {
                TAG_INDEX.delete(indexKey);
            }
        }
    }

    function tagPopularity(entry) {
        if (String(entry.is_deprecated || "") === "1") {
            return 0;
        }
        const value = Number(entry.rank_count || entry.rankCount || entry.post_count || entry.postCount || 0);
        return Number.isFinite(value) ? Math.max(0, value) : 0;
    }

    function editSimilarity(left, right) {
        if (!left || !right) {
            return 0;
        }
        if (left === right) {
            return 1;
        }
        const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
        const current = Array.from({ length: right.length + 1 }, () => 0);
        for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
            current[0] = leftIndex;
            for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
                const cost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
                current[rightIndex] = Math.min(previous[rightIndex] + 1, current[rightIndex - 1] + 1, previous[rightIndex - 1] + cost);
            }
            for (let index = 0; index < previous.length; index += 1) {
                previous[index] = current[index];
            }
        }
        return 1 - previous[right.length] / Math.max(left.length, right.length);
    }

    function englishTypoScore(query, candidate, weights) {
        if (!/^[a-z0-9]+$/.test(query) || !/^[a-z0-9]+$/.test(candidate) || query.length < 5) {
            return 0;
        }
        const lengthDelta = Math.abs(query.length - candidate.length);
        if (lengthDelta > Math.max(2, Math.floor(query.length / 4))) {
            return 0;
        }
        const ratio = editSimilarity(query, candidate);
        if (ratio < 0.88) {
            return 0;
        }
        return weights.prefix + Math.min((ratio - 0.88) * 900, 90) - Math.min(lengthDelta * 25, 70);
    }

    function usefulReverseCandidate(candidate) {
        if (/^[ぁぃぅぇぉっゃゅょゎ]/u.test(candidate)) {
            return false;
        }
        return candidate.length >= 3 || /[\u3400-\u9fff]/u.test(candidate);
    }

    function looseFieldScore(query, candidate, weights) {
        if (!query || !candidate) {
            return 0;
        }
        if (candidate === query) {
            return weights.exact;
        }
        if (candidate.startsWith(query)) {
            return weights.prefix + Math.min(query.length * 10, 90) - Math.min(Math.max(0, candidate.length - query.length) * 1.5, 90);
        }
        const index = candidate.indexOf(query);
        if (index >= 0) {
            return weights.contains + Math.min(query.length * 8, 90) - Math.min(index * 26, 180) - Math.min(Math.max(0, candidate.length - query.length) * 1.2, 120);
        }
        if (query.includes(candidate) && usefulReverseCandidate(candidate)) {
            return weights.reverse + Math.min(candidate.length * 8, 90);
        }
        if (query.length < 3 || candidate.length < 3) {
            return 0;
        }
        const typoScore = englishTypoScore(query, candidate, weights);
        if (typoScore > 0) {
            return typoScore;
        }
        let matched = 0;
        let cursor = 0;
        for (const char of query) {
            const found = candidate.indexOf(char, cursor);
            if (found >= 0) {
                matched += 1;
                cursor = found + 1;
            }
        }
        const orderScore = matched / Math.max(query.length, 1);
        return orderScore >= 0.84 ? weights.fuzzy + orderScore * 120 : 0;
    }

    function symbolFieldScore(query, candidate, weights) {
        if (!query || !candidate || !/[\p{P}\p{S}]/u.test(query)) {
            return 0;
        }
        const compactQuery = query.replace(/\s+/g, "");
        const compactCandidate = candidate.replace(/\s+/g, "");
        if (candidate === query || compactCandidate === compactQuery) {
            return weights.exact + 40;
        }
        if (query.length >= 2 && (candidate.startsWith(query) || compactCandidate.startsWith(compactQuery))) {
            return weights.prefix + 20;
        }
        if (query.length >= 2 && (candidate.includes(query) || compactCandidate.includes(compactQuery))) {
            return weights.contains;
        }
        return 0;
    }

    function fieldScore(query, symbolQuery, candidate, symbolCandidate, weights) {
        return Math.max(looseFieldScore(query, candidate, weights), symbolFieldScore(symbolQuery, symbolCandidate, weights));
    }

    function queryComponents(queryText) {
        const normalized = normalize(queryText);
        const aliasNormalized = normalizeComponentAlias(queryText);
        const seen = new Set();
        const components = [];
        const add = (component) => {
            if (component && !seen.has(component)) {
                seen.add(component);
                components.push(component);
            }
        };
        for (const item of LEXICON_TERMS) {
            const isColorTerm = item.components.some((component) => COLOR_COMPONENTS.has(component));
            if ((item.term.length >= 2 || isColorTerm) && normalized.includes(item.term)) {
                item.components.forEach(add);
            }
        }
        for (const alias of DIRECT_COMPONENT_ALIASES) {
            if (alias.terms.some((term) => {
                const normalizedTerm = normalizeComponentAlias(term);
                return normalizedTerm && aliasNormalized.includes(normalizedTerm);
            })) {
                alias.components.forEach(add);
            }
        }
        tagComponents(queryText).forEach(add);
        return components;
    }

    function queryFamilyComponents(queryText) {
        const normalized = normalizeComponentAlias(queryText);
        const seen = new Set();
        const components = [];
        const add = (component) => {
            if (component && !seen.has(component)) {
                seen.add(component);
                components.push(component);
            }
        };
        for (const alias of DIRECT_COMPONENT_ALIASES) {
            if (alias.terms.some((term) => {
                const normalizedTerm = normalizeComponentAlias(term);
                return normalizedTerm && normalized.includes(normalizedTerm);
            })) {
                alias.components.forEach(add);
            }
        }
        tagComponents(queryText).forEach(add);
        return components;
    }

    function componentScore(queryParts, item) {
        if (!queryParts.length || !item.components.length) {
            return 0;
        }
        const entryParts = new Set(item.components);
        let matched = 0;
        for (const part of queryParts) {
            if (entryParts.has(part)) {
                matched += 1;
            }
        }
        if (!matched) {
            return 0;
        }
        const ratio = matched / queryParts.length;
        const extra = Math.max(0, item.components.length - matched);
        if (matched === queryParts.length) {
            const base = extra === 0 ? 1180 : 980;
            return base + matched * 90 - Math.min(extra * 18, 160);
        }
        return 260 + ratio * 360 + matched * 35 - Math.min(extra * 8, 80);
    }

    function exactQueryTextMatch(query, symbolQuery, item) {
        if (!query && !symbolQuery) {
            return false;
        }
        if ((query && (item.ja === query || item.tag === query || item.displayTag === query)) || (symbolQuery && (item.jaSymbol === symbolQuery || item.tagSymbol === symbolQuery || item.displayTagSymbol === symbolQuery))) {
            return true;
        }
        return item.aliases.some((alias) => (query && alias.loose === query) || (symbolQuery && alias.symbol === symbolQuery));
    }

    function hasPlaceholderDisplay(entry) {
        return /(?:[○〇◯]{2,}|[～〜]+)/u.test(displayTagValue(entry));
    }

    function dictionaryScoreBoost(entry, baseScore) {
        return String(entry.is_dictionary || "") === "1" && baseScore >= 120 ? 190 : 0;
    }

    function importantTagBoost(entry, baseScore) {
        const importance = Number(entry.importance || 0);
        if (String(entry.is_dictionary || "") !== "1" || String(entry.is_deprecated || "") === "1" || !Number.isFinite(importance) || importance <= 0 || baseScore < 500) {
            return 0;
        }
        return Math.min(480, 180 * importance);
    }

    function placeholderPatternBoost(query, symbolQuery, item, baseScore) {
        if (String(item.entry.is_dictionary || "") !== "1" || !hasPlaceholderDisplay(item.entry) || baseScore < 500) {
            return 0;
        }
        return exactQueryTextMatch(query, symbolQuery, item) ? 320 : 0;
    }

    function exactCanonicalTagBoost(queryTagKey, item) {
        return queryTagKey && canonicalSearchTagKey(item.entry.tag) === queryTagKey ? 700 : 0;
    }

    function representativeScoreBoost(query, symbolQuery, item, baseScore) {
        if (String(item.entry.is_dictionary || "") !== "1" || String(item.entry.is_deprecated || "") === "1" || baseScore < 700 || !exactQueryTextMatch(query, symbolQuery, item)) {
            return 0;
        }
        const description = String(item.entry.description || "");
        return ["代表タグ", "基本的", "基本タグ"].some((marker) => description.includes(marker)) ? 220 : 0;
    }

    function dictionaryLeadingDescriptionBoost(query, item, baseScore) {
        if (String(item.entry.is_dictionary || "") !== "1" || String(item.entry.is_deprecated || "") === "1" || baseScore < 550 || query.length < 2) {
            return 0;
        }
        if (/[\u3040-\u30ff\u3400-\u9fff]/u.test(String(item.entry.ja || "")) || item.ja === query || item.tag === query) {
            return 0;
        }
        return item.description && item.description.startsWith(query) ? 520 : 0;
    }

    function dictionaryExactMatchBoost(query, symbolQuery, item, baseScore) {
        return String(item.entry.is_dictionary || "") === "1" && baseScore >= 700 && exactQueryTextMatch(query, symbolQuery, item) ? 260 : 0;
    }

    function verifiedTagExactMatchBoost(query, symbolQuery, item, baseScore) {
        if (String(item.entry.is_dictionary || "") === "1" || baseScore < 700) {
            return 0;
        }
        const hasVerifiedSource = String(item.entry.has_tag || "") === "1" || String(item.entry.has_wiki || "") === "1";
        return hasVerifiedSource && exactQueryTextMatch(query, symbolQuery, item) ? 360 : 0;
    }

    function dictionaryComponentFamilyBoost(familyParts, item, baseScore, hasDictionaryExactMatch, query, symbolQuery) {
        if (!familyParts.length || String(item.entry.is_dictionary || "") !== "1" || String(item.entry.is_deprecated || "") === "1") {
            return 0;
        }
        if (hasDictionaryExactMatch && !exactQueryTextMatch(query, symbolQuery, item)) {
            return 0;
        }
        if (!item.components.length) {
            return 0;
        }
        let matched = 0;
        const entryParts = new Set(item.components);
        for (const part of familyParts) {
            if (entryParts.has(part)) {
                matched += 1;
            }
        }
        if (!matched) {
            return 0;
        }
        const extra = Math.max(0, item.components.length - matched);
        const popularity = Math.max(Number(item.entry.rank_count || 0), Number(item.entry.post_count || 0), 0);
        const popularityBoost = Math.min(Math.log10(popularity + 10) * 38, 250);
        const markerBoost = /[〇○×Xx]{2}|タグ群|種類を指定|組み合わせ|組み合わせる|ほかにも|他に/.test(String(item.entry.description || "") + "," + String(item.entry.aliases || "")) ? 120 : 0;
        const hairContextPenalty = familyParts.length === 1 && familyParts[0] === "hair" && item.components.some((part) => HAIR_CONTEXT_PENALTY_COMPONENTS.has(part)) ? 260 : 0;
        const requiredContextParts = familyParts.filter((part) => FAMILY_REQUIRED_CONTEXT_COMPONENTS.has(part));
        const needsContext = familyParts.some((part) => FAMILY_OBJECT_COMPONENTS.has(part)) && requiredContextParts.length > 0;
        const missingContextPenalty = needsContext && !item.components.some((part) => requiredContextParts.includes(part)) ? 360 : 0;
        const targetScore = 1420 + matched * 80 + popularityBoost + markerBoost - Math.min(extra * 10, 150) - hairContextPenalty - missingContextPenalty;
        return Math.max(0, targetScore - baseScore);
    }

    function characterPartialNameBoost(query, item, baseScore) {
        if (String(item.entry.category || "") !== "4" || baseScore < 700 || query.length < 3) {
            return 0;
        }
        return item.ja && item.ja !== query && item.ja.includes(query) && !item.ja.startsWith(query) ? 360 : 0;
    }

    function properNamePartialPenalty(query, symbolQuery, item, baseScore) {
        if (String(item.entry.is_dictionary || "") === "1" || baseScore < 700 || query.length < 2 || exactQueryTextMatch(query, symbolQuery, item)) {
            return 0;
        }
        const partialJa = item.ja && item.ja !== query && item.ja.includes(query);
        const partialAlias = item.aliases.some((alias) => alias.loose && alias.loose !== query && alias.loose.includes(query));
        if (!partialJa && !partialAlias) {
            return 0;
        }
        const category = String(item.entry.category || "");
        if (category === "4") {
            return 300;
        }
        if (category === "1" || category === "3") {
            return 240;
        }
        return 170;
    }

    function lowSignalScorePenalty(entry, baseScore) {
        if (String(entry.is_dictionary || "") === "1" || baseScore < 700) {
            return 0;
        }
        const postCount = Number(entry.post_count || entry.postCount || 0);
        const rankCount = Number(entry.rank_count || entry.rankCount || 0);
        return ((!Number.isFinite(postCount) || postCount <= 0) && (!Number.isFinite(rankCount) || rankCount <= 0)) ? 180 : 0;
    }

    function isSearchableMatch(entry, score) {
        if (String(entry.is_dictionary || "") === "1") {
            return true;
        }
        const postCount = Number(entry.post_count || entry.postCount || 0);
        const rankCount = Number(entry.rank_count || entry.rankCount || 0);
        if ((Number.isFinite(postCount) && postCount > 0) || (Number.isFinite(rankCount) && rankCount > 0)) {
            return true;
        }
        const hasVerifiedSource = String(entry.has_tag || "") === "1" || String(entry.has_wiki || "") === "1";
        return hasVerifiedSource && score >= 1000;
    }

    function isDeprecatedVisibleForQuery(item, query, symbolQuery) {
        if (String(item.entry.is_deprecated || "") !== "1") {
            return true;
        }
        if (String(item.entry.is_dictionary || "") === "1" && hasPlaceholderDisplay(item.entry) && exactQueryTextMatch(query, symbolQuery, item)) {
            return true;
        }
        return Boolean((query && item.tag === query) || (symbolQuery && item.tagSymbol === symbolQuery));
    }

    function scoreEntry(query, symbolQuery, queryParts, familyParts, item, hasDictionaryExactMatch, queryTagKey) {
        let score = 0;
        let strongMatches = 0;
        const registerScore = (value) => {
            score = Math.max(score, value);
            if (value >= 400) {
                strongMatches += 1;
            }
        };
        registerScore(fieldScore(query, symbolQuery, item.ja, item.jaSymbol, { exact: 1200, prefix: 1030, contains: 820, reverse: 700, fuzzy: 180 }));
        registerScore(fieldScore(query, symbolQuery, item.tag, item.tagSymbol, { exact: 1250, prefix: 1060, contains: 850, reverse: 720, fuzzy: 190 }));
        registerScore(fieldScore(query, symbolQuery, item.displayTag, item.displayTagSymbol, { exact: 1250, prefix: 1060, contains: 850, reverse: 720, fuzzy: 190 }));
        registerScore(fieldScore(query, symbolQuery, item.description, item.descriptionSymbol, { exact: 760, prefix: 740, contains: 700, reverse: 420, fuzzy: 120 }));
        for (const alias of item.aliases) {
            registerScore(fieldScore(query, symbolQuery, alias.loose, alias.symbol, { exact: 1120, prefix: 980, contains: 760, reverse: 650, fuzzy: 160 }));
        }
        const baseScore = Math.max(score, componentScore(queryParts, item)) + Math.min(Math.max(0, strongMatches - 1) * 35, 90);
        const dictionaryBoostedScore = baseScore + dictionaryScoreBoost(item.entry, baseScore);
        const importantBoostedScore = dictionaryBoostedScore + importantTagBoost(item.entry, dictionaryBoostedScore);
        const placeholderBoostedScore = importantBoostedScore + placeholderPatternBoost(query, symbolQuery, item, importantBoostedScore);
        const exactTagBoostedScore = placeholderBoostedScore + exactCanonicalTagBoost(queryTagKey, item);
        const boostedScore = exactTagBoostedScore + dictionaryExactMatchBoost(query, symbolQuery, item, exactTagBoostedScore) + verifiedTagExactMatchBoost(query, symbolQuery, item, exactTagBoostedScore) + representativeScoreBoost(query, symbolQuery, item, exactTagBoostedScore) + dictionaryLeadingDescriptionBoost(query, item, exactTagBoostedScore) + characterPartialNameBoost(query, item, exactTagBoostedScore) + dictionaryComponentFamilyBoost(familyParts, item, exactTagBoostedScore, hasDictionaryExactMatch, query, symbolQuery);
        const deprecatedExactBoost = String(item.entry.is_deprecated || "") === "1" && isDeprecatedVisibleForQuery(item, query, symbolQuery) ? 620 : 0;
        return boostedScore + deprecatedExactBoost - lowSignalScorePenalty(item.entry, boostedScore);
    }

    function compareSearchMatches(left, right) {
        const scoreDelta = right.score - left.score;
        if (scoreDelta !== 0) {
            return scoreDelta;
        }
        const popularityDelta = right.popularity - left.popularity;
        if (popularityDelta !== 0) {
            return popularityDelta;
        }
        return String(left.entry.tag).localeCompare(String(right.entry.tag));
    }

    function pushTopSearchMatch(topMatches, candidate, limit) {
        let insertIndex = topMatches.length;
        for (let index = 0; index < topMatches.length; index += 1) {
            if (compareSearchMatches(candidate, topMatches[index]) < 0) {
                insertIndex = index;
                break;
            }
        }
        topMatches.splice(insertIndex, 0, candidate);
        if (topMatches.length > limit) {
            topMatches.pop();
        }
    }

    function directExactSearchMatch(query, symbolQuery, queryParts, familyParts, queryTagKey) {
        const entry = TAG_EXACT_INDEX.get(queryTagKey) || TAG_INDEX.get(queryTagKey);
        if (!entry) {
            return null;
        }
        const item = SEARCH_DATA_BY_KEY.get(debugEditKey(entry.tag));
        if (!item || !exactQueryTextMatch(query, symbolQuery, item)) {
            return null;
        }
        const score = scoreEntry(query, symbolQuery, queryParts, familyParts, item, true, queryTagKey);
        if (score < 120 || !isSearchableMatch(item.entry, score) || !isDeprecatedVisibleForQuery(item, query, symbolQuery)) {
            return null;
        }
        return { entry: item.entry, searchItem: item, score, popularity: item.popularity };
    }

    function shouldUseDirectExactFastPath(queryText, match) {
        if (!match) {
            return false;
        }
        const raw = String(queryText || "").trim();
        if (!raw || /[\u3040-\u30ff\u3400-\u9fff]/u.test(raw)) {
            return false;
        }
        const category = String(match.entry.category || "");
        return raw.includes("_") || category === "3" || category === "4";
    }

    function deprecatedAlternativeTags(entry) {
        const tags = [];
        const seen = new Set([canonicalKey(entry.tag)]);
        for (const source of [entry.wiki_note, entry.description].map((value) => String(value || ""))) {
            for (const match of source.matchAll(/(?:代替候補|代替タグ)[:：]\s*([^。．\n]+)/g)) {
                for (const tokenMatch of match[1].matchAll(/[a-z0-9][a-z0-9_:+!'./-]*/gi)) {
                    const token = tokenMatch[0].replace(/[.,;:]+$/g, "");
                    const candidate = TAG_INDEX.get(canonicalKey(token));
                    const tag = candidate ? candidate.tag : token;
                    const key = canonicalKey(tag);
                    if (!key || seen.has(key)) {
                        continue;
                    }
                    seen.add(key);
                    tags.push(tag);
                }
            }
        }
        return tags;
    }

    function relatedTagCandidate(value) {
        const candidate = TAG_INDEX.get(canonicalKey(value));
        if (!candidate || !isSearchableMatch(candidate, 999)) {
            return null;
        }
        return candidate;
    }

    function addRelatedTag(related, seen, value) {
        const candidate = relatedTagCandidate(value);
        if (!candidate) {
            return false;
        }
        const key = canonicalKey(candidate.tag);
        if (seen.has(key)) {
            return false;
        }
        seen.add(key);
        related.push(candidate.tag);
        return true;
    }

    function phraseWords(value) {
        return String(value || "").toLowerCase().replaceAll("_", " ").match(/[a-z0-9][a-z0-9'-]*/g) || [];
    }

    function englishTagPhrases(value) {
        const words = phraseWords(value);
        const phrases = [];
        const seen = new Set();
        for (let index = 0; index < words.length; index += 1) {
            const maxSize = Math.min(5, words.length - index);
            for (let size = maxSize; size >= 1; size -= 1) {
                const phraseWords = words.slice(index, index + size);
                if (size === 1 && RELATED_WORD_STOPWORDS.has(phraseWords[0])) {
                    continue;
                }
                const phrase = phraseWords.join(" ");
                const key = canonicalKey(phrase);
                if (!seen.has(key)) {
                    seen.add(key);
                    phrases.push(phrase);
                }
            }
        }
        return phrases;
    }

    function underscoredDescriptionTags(value) {
        const tags = [];
        const seen = new Set();
        for (const match of String(value || "").matchAll(/[a-z0-9][a-z0-9_:+!'./()\\-]*_[a-z0-9_:+!'./()\\-]*/gi)) {
            const token = match[0].replace(/^[.,;:!?]+|[.,;:!?]+$/g, "");
            const candidate = relatedTagCandidate(token);
            if (!candidate) {
                continue;
            }
            const key = canonicalKey(candidate.tag);
            if (seen.has(key)) {
                continue;
            }
            seen.add(key);
            tags.push(candidate.tag);
        }
        return tags;
    }

    function relatedTags(entry) {
        const seen = new Set([canonicalKey(entry.tag)]);
        const related = [];
        const override = relatedTagsAreOverridden(entry);
        const directTerms = override ? splitAliases(relatedTagsValue(entry)) : splitAliases(relatedTagsValue(entry)).concat(splitAliases(entry.aliases), splitAliases(entry.search_aliases));
        for (const term of directTerms) {
            addRelatedTag(related, seen, term);
            if (related.length >= RELATED_TAG_LIMIT) {
                return related;
            }
        }
        for (const tag of underscoredDescriptionTags(descriptionValue(entry))) {
            addRelatedTag(related, seen, tag);
            if (related.length >= RELATED_TAG_LIMIT) {
                return related;
            }
        }
        if (override) {
            return related;
        }
        const blockedSingleWords = new Set();
        for (const phrase of englishTagPhrases(descriptionValue(entry))) {
            const words = phraseWords(phrase);
            if (words.length === 1 && blockedSingleWords.has(words[0])) {
                continue;
            }
            const added = addRelatedTag(related, seen, phrase);
            if (added && words.length > 1) {
                for (const word of words) {
                    if (!RELATED_WORD_STOPWORDS.has(word)) {
                        blockedSingleWords.add(word);
                    }
                }
            }
            if (related.length >= RELATED_TAG_LIMIT) {
                return related;
            }
        }
        return related;
    }

    function cleanEntryForPost(entry) {
        const copy = { ...entry };
        delete copy._pdIndexKeys;
        return copy;
    }

    function searchEntries(queryText) {
        const query = normalize(queryText);
        const symbolQuery = normalizeSymbol(queryText);
        const queryTagKey = canonicalSearchTagKey(queryText);
        if (!query && !symbolQuery) {
            return [];
        }
        const queryParts = queryComponents(queryText);
        const familyParts = queryFamilyComponents(queryText);
        const hasDictionaryExactMatch = SEARCH_DATA.some((item) => String(item.entry.is_dictionary || "") === "1" && exactQueryTextMatch(query, symbolQuery, item));
        const directMatch = directExactSearchMatch(query, symbolQuery, queryParts, familyParts, queryTagKey);
        if (shouldUseDirectExactFastPath(queryText, directMatch)) {
            return [directMatch];
        }
        const matches = [];
        for (const item of SEARCH_DATA) {
            let score = scoreEntry(query, symbolQuery, queryParts, familyParts, item, hasDictionaryExactMatch, queryTagKey);
            if (hasDictionaryExactMatch) {
                score -= properNamePartialPenalty(query, symbolQuery, item, score);
            }
            if (score < 120 || !isSearchableMatch(item.entry, score) || !isDeprecatedVisibleForQuery(item, query, symbolQuery)) {
                continue;
            }
            pushTopSearchMatch(matches, { entry: item.entry, searchItem: item, score, popularity: item.popularity }, MAX_RESULTS);
        }
        return matches;
    }

    function searchPayload(queryText) {
        return searchEntries(queryText).map((match) => ({
            entry: cleanEntryForPost(match.entry),
            score: match.score,
            popularity: match.popularity,
            related: relatedTags(match.entry),
            deprecatedAlternatives: deprecatedAlternativeTags(match.entry)
        }));
    }

    function upsertEntry(row) {
        if (!row || !row.tag) {
            return;
        }
        const key = debugEditKey(row.tag);
        let entry = DATA.find((item) => debugEditKey(item.tag) === key);
        if (!entry) {
            entry = { ...row };
            DATA.push(entry);
        } else {
            applyRowToEntry(entry, row);
        }
        refreshSearchIndexEntry(entry);
    }

    function initialize(scripts) {
        importScripts.apply(null, scripts || []);
        const rawData = Array.isArray(self.PROMPT_DICTIONARY_DATA) ? self.PROMPT_DICTIONARY_DATA : [];
        const rawEdits = Array.isArray(self.PROMPT_DICTIONARY_EDITS) ? self.PROMPT_DICTIONARY_EDITS : [];
        QUERY_LEXICON = self.PROMPT_DICTIONARY_LEXICON || {};
        DATA = applyDictionaryEdits(normalizeDictionaryData(rawData), rawEdits);
        LEXICON_TERMS = buildLexiconTerms(QUERY_LEXICON);
        rebuildSearchIndexes();
    }

    self.addEventListener("message", (event) => {
        const message = event.data || {};
        try {
            if (message.type === "init") {
                initialize(message.scripts || []);
                self.postMessage({ type: "ready", requestId: message.requestId || "", count: DATA.length });
                return;
            }
            if (message.type === "search") {
                self.postMessage({ type: "searchResult", requestId: message.requestId || "", query: message.query || "", matches: searchPayload(message.query || ""), count: DATA.length });
                return;
            }
            if (message.type === "upsert") {
                upsertEntry(message.entry);
                self.postMessage({ type: "updated", requestId: message.requestId || "", count: DATA.length });
                return;
            }
            if (message.type === "remove") {
                removeSearchIndexEntry(message.tag);
                self.postMessage({ type: "updated", requestId: message.requestId || "", count: DATA.length });
            }
        } catch (error) {
            self.postMessage({ type: "error", requestId: message.requestId || "", message: error && error.message ? error.message : String(error || "Worker error") });
        }
    });
})();
