import type { TickerSearchInstrumentClass, TickerSearchRankableItem, } from "./types"; import { getYahooSymbol, tickerHasYahooSuffix } from "../../sources/yahoo-finance/symbols"; import { canonicalExchange, parsePublicTickerKey } from "../../utils/exchanges"; const FUND_TYPES = new Set(["ETF", "ETN", "ETP", "FUND", "MUTUALFUND", "CEF", "CLOSEDEND"]); const DERIVATIVE_TYPES = new Set(["OPT", "OPTION", "OPTIONS", "FUT", "FUTURE", "FUTURES", "WARRANT", "WARRANTS", "RIGHT", "RIGHTS"]); const EQUITY_TYPES = new Set(["STK", "STOCK", "EQUITY", "COMMONSTOCK", "COMMON STOCK", "ADR", "DEPOSITARY RECEIPT", "DEPOSITARYRECEIPT", "ORDINARYSHARES", "ORDINARY SHARES"]); const COMPANY_NAME_SUFFIXES = new Set([ "AG", "AKTIENGESELLSCHAFT", "CO", "COMPANY", "CORP", "CORPORATION", "INC", "INCORPORATED", "LTD", "LIMITED", "NV", "PLC", "R", "REGISTERED", "SA", "SE", ]); const EXCHANGE_HINT_ALIASES: Record = { NASDAQ: ["NASDAQ", "NMS"], NYSE: ["NYSE", "NYSE ARCA", "ARCA"], AMEX: ["AMEX"], TSX: ["TSX", "TORONTO"], TORONTO: ["TORONTO", "TSX"], XETRA: ["XETRA"], TSE: ["TOKYO STOCK EXCHANGE", "TOKYO", "JPX", "TSE"], TOKYO: ["TOKYO STOCK EXCHANGE", "TOKYO", "JPX", "TSE"], JPX: ["JPX", "TOKYO STOCK EXCHANGE", "TOKYO"], LSE: ["LSE", "LONDON"], HKEX: ["HKEX", "HONG KONG"], SWISS: ["SWISS", "SIX"], BUE: ["BUENOS AIRES", "BUE"], BUENOS: ["BUENOS AIRES", "BUE"], AIRES: ["BUENOS AIRES", "BUE"], }; const ASSET_HINT_MAP: Record = { STOCK: "equity", STK: "equity", EQUITY: "equity", SHARE: "equity", SHARES: "equity", COMMON: "equity", ETF: "fund", ETN: "fund", ETP: "fund", FUND: "fund", OPTION: "derivative", OPTIONS: "derivative", CALL: "derivative", PUT: "derivative", WARRANT: "derivative", WARRANTS: "derivative", FUTURE: "derivative", FUTURES: "derivative", }; const SAVED_MATCH_BONUS = 900; /** * How many times more popular a company whose name only starts with the query * must be to lead one named exactly as typed. TSMC is searched about five * times as often as Taiwan Semiconductor Co.; Siemens Energy about as often as * Siemens AG, so there the exact name keeps leading. */ const POPULARITY_LEAD_RATIO = 2; interface SearchQueryIntent { rawQuery: string; normalizedQuery: string; compactQuery: string; companyQuery: string; companyQueryKey: string; exchangeHints: string[]; assetPreference: TickerSearchInstrumentClass | null; } export function findExactTickerSearchMatch & Partial>( items: T[], query: string, ): T | null { const literal = items.find((item) => normalizeTickerSymbol(item.symbol || item.label) === normalizeTickerSymbol(query)); if (literal) return literal; if (isQualifiedTickerQuery(query)) { return items.find((item) => matchesQualifiedTicker(item, query)) ?? null; } const aliasForms = buildSymbolAliases(query); const normalizedAliases = new Set(aliasForms.map((value) => normalizeSearchText(value))); const compactAliases = new Set(aliasForms.map((value) => compactSearchText(value))); return items.find((item) => !isExplicitMarketSymbol(item.symbol || item.label) && !isCryptoInstrumentType(item.instrumentType) && getItemSearchAliases(item).some((alias) => normalizedAliases.has(normalizeSearchText(alias)) || compactAliases.has(compactSearchText(alias)) ) ) ?? null; } export function isCryptoInstrumentType(type?: string): boolean { return ["CRYPTO", "CRYPTOCURRENCY", "DIGITALCURRENCY"].includes((type ?? "").toUpperCase().replace(/[\s_-]/g, "")); } function isQualifiedTickerQuery(query: string): boolean { const symbol = normalizeTickerSymbol(query); return isExplicitMarketSymbol(symbol) || !!parsePublicTickerKey(symbol).exchange || tickerHasYahooSuffix(symbol); } /** Futures, currency pairs, and indices use punctuation as part of their identity. */ export function isExplicitMarketSymbol(symbol: string): boolean { return /[=^]/.test(symbol) || /^[A-Z]{3}\/[A-Z]{3}$/i.test(symbol.trim()); } function forexPair(symbol: string): string | null { if (/^[A-Z]{3}\/[A-Z]{3}$/.test(symbol)) return symbol.replace("/", ""); if (/^[A-Z]{6}=X$/.test(symbol)) return symbol.slice(0, -2); if (/^[A-Z]{3}=X$/.test(symbol)) return `USD${symbol.slice(0, -2)}`; return null; } export function getForexQuoteCurrency(symbol: string): string | undefined { return forexPair(parsePublicTickerKey(normalizeTickerSymbol(symbol)).symbol)?.slice(3); } /** Venue and market syntax must survive exact matching. Only explicit FX forms are equivalent. */ function matchesQualifiedTicker( item: Pick & Partial, query: string, ): boolean { const normalized = normalizeTickerSymbol(query); const symbol = normalizeTickerSymbol(item.symbol || item.label); if (symbol === normalized) return true; const requested = parsePublicTickerKey(normalized); const candidate = parsePublicTickerKey(symbol); const exchanges = [candidate.exchange, item.exchangeLabel, item.primaryExchangeLabel, item.right] .filter((value): value is string => !!value); if (requested.exchange) { return candidate.symbol === requested.symbol && exchanges.some((exchange) => canonicalExchange(exchange) === requested.exchange); } if (isExplicitMarketSymbol(requested.symbol)) { const pair = forexPair(requested.symbol); return candidate.symbol === requested.symbol || (pair != null && forexPair(candidate.symbol) === pair); } return exchanges.some((exchange) => getYahooSymbol(candidate.symbol, exchange).toUpperCase() === normalized); } function getTickerSearchListingKey(item: Pick & Partial): string { const parsed = parsePublicTickerKey(normalizeTickerSymbol(item.symbol || item.label)); const exchange = parsed.exchange || (item.exchangeLabel === "SMART" ? item.primaryExchangeLabel : item.exchangeLabel || item.primaryExchangeLabel || item.right); return `${parsed.symbol}|${canonicalExchange(exchange)}${item.contractKey ? `|${item.contractKey}` : ""}`; } export function rankTickerSearchItems & Partial>( items: T[], query: string, ): T[] { const intent = analyzeSearchQuery(query); if (!intent.normalizedQuery) return items; const ranked = items .map((item, index) => { // A saved public key replaces its provider row during deduplication. // Score its exact bare symbol like that row, retaining the saved identity // and the resolver's stricter aliases outside this ranking projection. const listing = parsePublicTickerKey(item.symbol || item.label); const exactListingSymbol = listing.exchange && !isQualifiedTickerQuery(query) && listing.symbol === normalizeTickerSymbol(query) ? listing.symbol : null; const normalizedLabel = normalizeSearchText(exactListingSymbol ?? item.label); const normalizedDetail = normalizeSearchText(item.detail); const normalizedRight = normalizeSearchText(item.right || ""); const companyNameKey = getCompanyNameKey(item.detail); const textQueries = [intent.normalizedQuery, intent.companyQuery].filter(Boolean); const labelScore = maxScoreForQueries(textQueries, normalizedLabel, { exact: 24_000, prefix: 18_000, substring: 14_000, fuzzy: 7_000, }); const detailScore = Math.max( maxScoreForQueries(textQueries, normalizedDetail, { exact: 4_500, prefix: 3_800, substring: 2_600, fuzzy: 600, }), maxScoreForQueries(textQueries, normalizedRight, { exact: 1_200, prefix: 1_000, substring: 700, fuzzy: 100, }), ); const aliases = getItemSearchAliases(item); const aliasScore = (exactListingSymbol ? [...aliases, exactListingSymbol] : aliases) .reduce((best, alias) => Math.max(best, scoreSearchAlias(intent, alias)), 0); const textScore = labelScore + detailScore + aliasScore + (isQualifiedTickerQuery(query) && matchesQualifiedTicker(item, query) ? 100_000 : 0); const saved = isSavedSearchItem(item); const explicitIntentScore = scoreAssetPreference(intent, item.instrumentClass) + scoreExchangePreference(intent, item); const priorityScore = explicitIntentScore + scoreListingPriority(item); const companyMatchRank = scoreCompanyNameMatch(intent, companyNameKey); return { item, index, companyMatchRank, companyRank: companyMatchRank, companyNameKey, issuerGroupKey: companyMatchRank > 0 && item.instrumentClass === "equity" ? getIssuerGroupKey(item.detail) : null, explicitIntentScore, normalizedSymbol: normalizeSearchText(item.symbol || item.label), symbolMatchRank: scoreSymbolMatchRank(intent, item), nameOnly: companyMatchRank > 0 && labelScore + aliasScore <= 0 && !(isQualifiedTickerQuery(query) && matchesQualifiedTicker(item, query)), textScore, score: textScore + priorityScore + (textScore > 0 && saved ? SAVED_MATCH_BONUS : 0), }; }); // A saved symbol replaces its own source listing, not every exchange using it. const matchedLocalListings = new Set( ranked .filter(({ item, textScore }) => textScore > 0 && item.kind === "ticker") .map(({ item }) => getTickerSearchListingKey(item)), ); const filtered = ranked.filter(({ item, textScore }) => { if (textScore <= 0) return false; if (isExplicitMarketSymbol(query) && !isExplicitMarketSymbol(item.symbol || item.label)) return false; if (item.kind !== "search") return true; return !matchedLocalListings.has(getTickerSearchListingKey(item)); }); promoteMuchMorePopularCompanies(filtered); type RankedEntry = (typeof ranked)[number]; const compareFallbackEntries = (a: RankedEntry, b: RankedEntry): number => { if (b.symbolMatchRank !== a.symbolMatchRank) return b.symbolMatchRank - a.symbolMatchRank; if (b.score !== a.score) return b.score - a.score; const aSaved = isSavedSearchItem(a.item); const bSaved = isSavedSearchItem(b.item); if (aSaved !== bSaved) return aSaved ? -1 : 1; if (a.item.label.length !== b.item.label.length) return a.item.label.length - b.item.label.length; return a.index - b.index; }; const hasExplicitHint = intent.exchangeHints.length > 0 || intent.assetPreference != null; const bucketGroups = new Map>(); for (const entry of filtered) { const exactSymbolRank = entry.symbolMatchRank >= 2 ? entry.symbolMatchRank : 0; const bucketKey = [ exactSymbolRank, hasExplicitHint ? entry.explicitIntentScore : 0, entry.companyRank, ].join(":"); const groupKey = entry.issuerGroupKey ? `issuer:${entry.issuerGroupKey}` : `item:${entry.index}`; const groups = bucketGroups.get(bucketKey) ?? new Map(); const group = groups.get(groupKey) ?? []; group.push(entry); groups.set(groupKey, group); bucketGroups.set(bucketKey, groups); } const groupOrderByIndex = new Map(); for (const groups of bucketGroups.values()) { const orderedGroups = [...groups.values()] .map((entries) => ({ entries, representative: entries.reduce((best, entry) => compareFallbackEntries(entry, best) < 0 ? entry : best ), nameOnly: entries.every((entry) => entry.nameOnly && entry.symbolMatchRank === 0), saved: entries.some((entry) => isSavedSearchItem(entry.item)), providerRank: Math.min(...entries.map((entry) => entry.item.providerRank ?? Number.POSITIVE_INFINITY)), })) .sort((a, b) => { // Companies matched by name alone differ in text score only by name // and type length. The provider's popularity order is the better // signal there; a symbol match keeps its text relevance. if (a.nameOnly !== b.nameOnly) return a.nameOnly ? 1 : -1; if (a.nameOnly) { if (a.saved !== b.saved) return a.saved ? -1 : 1; if (a.providerRank !== b.providerRank) return a.providerRank - b.providerRank; } return compareFallbackEntries(a.representative, b.representative); }); orderedGroups.forEach(({ entries }, order) => { entries.forEach((entry) => groupOrderByIndex.set(entry.index, order)); }); } filtered.sort((a, b) => { // Precedence is exact symbol, explicit query hints, whole-word company // relevance, issuer relevance, provider order within that issuer, partial // symbol, saved relevance, then stable source order. const aExactSymbolRank = a.symbolMatchRank >= 2 ? a.symbolMatchRank : 0; const bExactSymbolRank = b.symbolMatchRank >= 2 ? b.symbolMatchRank : 0; if (bExactSymbolRank !== aExactSymbolRank) return bExactSymbolRank - aExactSymbolRank; if (hasExplicitHint && b.explicitIntentScore !== a.explicitIntentScore) { return b.explicitIntentScore - a.explicitIntentScore; } if (b.companyRank !== a.companyRank) { return b.companyRank - a.companyRank; } const aGroupOrder = groupOrderByIndex.get(a.index) ?? a.index; const bGroupOrder = groupOrderByIndex.get(b.index) ?? b.index; if (aGroupOrder !== bGroupOrder) return aGroupOrder - bGroupOrder; if ( a.issuerGroupKey && a.issuerGroupKey === b.issuerGroupKey ) { // Letters mixed with digits (4NVDA, NVDC34, 0R1I) mark a leveraged // product, a depositary receipt or a secondary venue's code filed under // the issuer's name, never its primary listing. All-digit symbols are // Tokyo, Shanghai and Hong Kong primaries and must not be touched. // Provider order is trusted for real listings of one issuer, but it put // a Milan 4x product above NVDA, so mixed codes yield first. const aSynthetic = isMixedCode(a.item.label); const bSynthetic = isMixedCode(b.item.label); if (aSynthetic !== bSynthetic) return aSynthetic ? 1 : -1; // A company searched by name leads with its home listing when the // provider types the other line as a receipt. An exact ticker search // (ASML) keeps provider order, and untyped ADRs (Yahoo's TM) cannot be // told apart from a second home listing, so they stay in provider order. if (a.nameOnly && b.nameOnly) { const aReceipt = isDepositaryReceiptType(a.item.instrumentType); const bReceipt = isDepositaryReceiptType(b.item.instrumentType); if (aReceipt !== bReceipt) return aReceipt ? 1 : -1; } const aProviderRank = a.item.providerRank ?? Number.POSITIVE_INFINITY; const bProviderRank = b.item.providerRank ?? Number.POSITIVE_INFINITY; if (aProviderRank !== bProviderRank) return aProviderRank - bProviderRank; } return compareFallbackEntries(a, b); }); const deduped: T[] = []; const seen = new Set(); for (const entry of filtered) { const key = getTickerSearchDedupKey(entry.item); if (seen.has(key)) continue; seen.add(key); deduped.push(entry.item); } return deduped; } // Folding accents keeps "Nestlé" matchable as "nestle" instead of "NESTL". function foldSearchText(text: string): string { return text.normalize("NFKD").replace(/\p{M}/gu, "").trim().toUpperCase(); } export function normalizeSearchText(text: string): string { return foldSearchText(text).replace(/[^A-Z0-9]+/g, " ").trim(); } export function compactSearchText(text: string): string { return foldSearchText(text).replace(/[^A-Z0-9]+/g, ""); } export function normalizeTickerSymbol(symbol: string): string { return symbol.trim().toUpperCase(); } export function buildSymbolAliases(symbol: string): string[] { const normalizedSymbol = normalizeTickerSymbol(symbol); if (!normalizedSymbol) return []; if (isExplicitMarketSymbol(normalizedSymbol)) { const pair = forexPair(normalizedSymbol); return pair ? [...new Set([normalizedSymbol, `${pair}=X`, `${pair.slice(0, 3)}/${pair.slice(3)}`])] : [normalizedSymbol]; } const searchText = normalizeSearchText(normalizedSymbol); const aliases = new Set([ normalizedSymbol, searchText, compactSearchText(normalizedSymbol), ]); if (searchText.includes(" ")) { aliases.add(searchText.replace(/ /g, ".")); aliases.add(searchText.replace(/ /g, "-")); } return [...aliases].filter(Boolean); } export function classifyInstrumentKind(rawType?: string): TickerSearchInstrumentClass { const normalizedType = normalizeSearchText(rawType || ""); if (!normalizedType) return "other"; if (FUND_TYPES.has(normalizedType)) return "fund"; if (DERIVATIVE_TYPES.has(normalizedType)) return "derivative"; if (EQUITY_TYPES.has(normalizedType)) return "equity"; if (normalizedType.includes("ETF") || normalizedType.includes("FUND")) return "fund"; if (normalizedType.includes("OPT") || normalizedType.includes("FUT") || normalizedType.includes("WARRANT")) return "derivative"; if (normalizedType.includes("EQUITY") || normalizedType.includes("STOCK") || normalizedType.includes("STK")) return "equity"; return "other"; } function analyzeSearchQuery(query: string): SearchQueryIntent { const normalizedQuery = normalizeSearchText(query); const compactQuery = compactSearchText(query); const tokens = normalizedQuery.split(" ").filter(Boolean); const exchangeHints = Array.from(new Set(tokens.filter((token) => token in EXCHANGE_HINT_ALIASES))); const assetHints = Array.from(new Set( tokens .map((token) => ASSET_HINT_MAP[token]) .filter((token): token is TickerSearchInstrumentClass => token != null), )); const assetPreference = assetHints.length === 1 ? assetHints[0]! : null; const companyTokens = tokens.filter((token) => !(token in EXCHANGE_HINT_ALIASES) && !(token in ASSET_HINT_MAP)); const companyQuery = companyTokens.join(" "); return { rawQuery: query, normalizedQuery, compactQuery, companyQuery, companyQueryKey: normalizeCompanyName(companyQuery), exchangeHints, assetPreference, }; } function getItemSearchAliases(item: Pick & Partial): string[] { const aliases = item.searchAliases && item.searchAliases.length > 0 ? item.searchAliases : buildSymbolAliases(item.symbol || item.label); return aliases.length > 0 ? aliases : [item.label]; } function maxScoreForQueries( queries: string[], value: string, weights: { exact: number; prefix: number; substring: number; fuzzy: number }, ): number { let best = 0; for (const query of queries) { best = Math.max(best, scoreSearchField(query, value, weights)); } return best; } function scoreSearchAlias(intent: SearchQueryIntent, alias: string): number { if (!alias) return 0; const normalizedAlias = normalizeSearchText(alias); const compactAlias = compactSearchText(alias); let score = scoreSearchField(intent.normalizedQuery, normalizedAlias, { exact: 8_000, prefix: 5_000, substring: 3_400, fuzzy: 1_200, }); if (intent.companyQuery && intent.companyQuery !== intent.normalizedQuery) { score = Math.max(score, scoreSearchField(intent.companyQuery, normalizedAlias, { exact: 6_000, prefix: 4_000, substring: 2_600, fuzzy: 800, })); } if (intent.compactQuery && compactAlias) { if (compactAlias === intent.compactQuery) { score = Math.max(score, 40_000 - compactAlias.length); } else if (compactAlias.startsWith(intent.compactQuery)) { score = Math.max(score, 6_000 - compactAlias.length); } } return score; } function scoreSymbolMatchRank( intent: SearchQueryIntent, item: Pick & Partial, ): number { if (normalizeTickerSymbol(item.symbol || item.label) === normalizeTickerSymbol(intent.rawQuery)) return 4; if (isQualifiedTickerQuery(intent.rawQuery)) return matchesQualifiedTicker(item, intent.rawQuery) ? 4 : 0; if (parsePublicTickerKey(item.symbol || item.label).symbol === normalizeTickerSymbol(intent.rawQuery)) return 4; if (isCryptoInstrumentType(item.instrumentType)) return 0; if (isExplicitMarketSymbol(item.symbol || item.label)) return 0; if (!intent.normalizedQuery && !intent.compactQuery) return 0; const displaySymbol = normalizeSearchText(item.symbol || item.label); const compactDisplaySymbol = compactSearchText(item.symbol || item.label); if ( displaySymbol === intent.normalizedQuery || (intent.compactQuery && compactDisplaySymbol === intent.compactQuery) ) { return 3; } const aliases = getItemSearchAliases(item); if (aliases.some((alias) => { const normalizedAlias = normalizeSearchText(alias); const compactAlias = compactSearchText(alias); return normalizedAlias === intent.normalizedQuery || (intent.compactQuery && compactAlias === intent.compactQuery); })) { return 2; } if ( displaySymbol.startsWith(intent.normalizedQuery) || (intent.compactQuery && compactDisplaySymbol.startsWith(intent.compactQuery)) ) { return 1; } return 0; } function scoreAssetPreference(intent: SearchQueryIntent, instrumentClass?: TickerSearchInstrumentClass): number { const itemClass = instrumentClass || "other"; if (!intent.assetPreference) { if (itemClass === "equity") return 400; if (itemClass === "fund") return -250; if (itemClass === "derivative") return -500; return 0; } if (itemClass === intent.assetPreference) return 2_400; if (itemClass === "other") return -300; return -1_200; } function scoreExchangePreference( intent: SearchQueryIntent, item: Pick & Partial, ): number { if (intent.exchangeHints.length === 0) return 0; const exchangeTexts = [ item.exchangeLabel, item.primaryExchangeLabel, item.right, ] .map((value) => normalizeSearchText(value || "")) .filter(Boolean); if (exchangeTexts.length === 0) return -400; const matchesHint = intent.exchangeHints.some((hint) => (EXCHANGE_HINT_ALIASES[hint] ?? [hint]).some((alias) => { const normalizedAlias = normalizeSearchText(alias); return exchangeTexts.some((exchangeText) => exchangeText.includes(normalizedAlias)); }) ); return matchesHint ? 2_000 : -800; } function isDepositaryReceiptType(type?: string): boolean { const normalized = normalizeSearchText(type || ""); return normalized.includes("DEPOSITARY") || ["ADR", "ADS", "GDR", "CDR", "BDR"].includes(normalized); } /** * A name typed exactly ("Taiwan Semiconductor") normally beats names that only * start with it, which keeps Apple Inc. above Apple Hospitality. When the * provider scores popularity, a company searched at least * POPULARITY_LEAD_RATIO times as often as every exactly named one that it * scored leads instead. Without scores the order is unchanged. */ function promoteMuchMorePopularCompanies(entries: Array<{ item: Partial; index: number; companyMatchRank: number; companyRank: number; issuerGroupKey: string | null; }>): void { const issuerKey = (entry: (typeof entries)[number]) => entry.issuerGroupKey ?? `item:${entry.index}`; const popularity = new Map(); for (const entry of entries) { const score = entry.item.popularity; if (typeof score !== "number" || !Number.isFinite(score)) continue; popularity.set(issuerKey(entry), Math.max(score, popularity.get(issuerKey(entry)) ?? score)); } if (popularity.size === 0) return; let exactPopularity = 0; for (const entry of entries) { if (entry.companyMatchRank === 3) exactPopularity = Math.max(exactPopularity, popularity.get(issuerKey(entry)) ?? 0); } if (exactPopularity <= 0) return; for (const entry of entries) { const score = popularity.get(issuerKey(entry)); if (entry.companyMatchRank === 2 && score != null && score >= exactPopularity * POPULARITY_LEAD_RATIO) { entry.companyRank = 4; } } } function isMixedCode(label: string): boolean { const symbol = label.split(".")[0] ?? label; return /\d/.test(symbol) && /[A-Za-z]/.test(symbol); } function scoreListingPriority(item: Pick & Partial): number { const instrumentClass = item.instrumentClass || "other"; if (instrumentClass !== "equity") return 0; let score = 180; if (item.label.includes(".")) score -= 140; if (item.label.length <= 5) score += 120; return score; } function getCompanyNameKey(detail: string): string { return normalizeCompanyName(detail.split("|")[0] || ""); } function getIssuerGroupKey(detail: string): string { // Listing descriptions do not create a different issuer. Strip only these // recognized tails for grouping, preserving full names and query relevance. const issuer = normalizeSearchText(detail.split("|")[0] || "") .replace(/\s+(?:(?:NY|NEW YORK) REGISTERED SHARES|DEPOSITARY RECEIPTS?)$/, ""); return normalizeCompanyName(issuer); } function normalizeCompanyName(name: string): string { const tokens = normalizeSearchText(name).split(" ").filter(Boolean); while (tokens.length > 1) { if (COMPANY_NAME_SUFFIXES.has(tokens.at(-1)!)) { tokens.pop(); continue; } const punctuatedSuffix = [...COMPANY_NAME_SUFFIXES].find((suffix) => suffix.length > 1 && tokens.length > suffix.length && tokens.slice(-suffix.length).every((token, index) => token.length === 1 && token === suffix[index] ) ); if (!punctuatedSuffix) break; tokens.splice(-punctuatedSuffix.length); } return tokens.join(" "); } function scoreCompanyNameMatch(intent: SearchQueryIntent, companyName: string): number { const query = intent.companyQueryKey; if (!query || !companyName) return 0; if (companyName === query) return 3; if (companyName.startsWith(`${query} `)) return 2; if (companyName.includes(` ${query} `) || companyName.endsWith(` ${query}`)) return 1; return 0; } function isSavedSearchItem(item: Pick & Partial): boolean { return item.saved === true || item.kind === "ticker" || item.category === "Saved" || item.category === "Open"; } function scoreSearchField(query: string, value: string, weights: { exact: number; prefix: number; substring: number; fuzzy: number }): number { if (!query || !value) return 0; if (value === query) return weights.exact - value.length; if (value.startsWith(query)) return weights.prefix - value.length; const substringIndex = value.indexOf(query); if (substringIndex >= 0) { return weights.substring - substringIndex * 25 - value.length; } let qi = 0; let score = 0; for (let i = 0; i < value.length && qi < query.length; i++) { if (value[i] !== query[qi]) continue; score += i === 0 || value[i - 1] === " " ? 10 : 2; qi += 1; } return qi === query.length ? weights.fuzzy + score : 0; } function getTickerSearchDedupKey(item: Pick & Partial): string { if (item.kind !== "ticker" && item.kind !== "search") return item.id; return getTickerSearchListingKey(item); }