function normalizeSearchText(value) { return String(value ?? '').trim().toLocaleLowerCase().replace(/\s+/g, ' '); } function ingredientNames(recipe) { return (recipe.ingredientGroups ?? []) .flatMap((group) => group.items ?? []) .map((item) => item.name) .filter(Boolean); } export function rankRecipeMatch(recipe, query) { const normalizedQuery = normalizeSearchText(query); if (!normalizedQuery) return { score: 0, matchedIngredients: [] }; const ingredients = ingredientNames(recipe); const matchedIngredients = ingredients.filter((name) => ( normalizeSearchText(name).includes(normalizedQuery) )); const hasExactIngredient = matchedIngredients.some((name) => ( normalizeSearchText(name) === normalizedQuery )); const title = normalizeSearchText(recipe.title); const tags = (recipe.tags ?? []).map(normalizeSearchText); const summary = normalizeSearchText(recipe.summary); let score = 0; if (hasExactIngredient) score = 500; else if (matchedIngredients.length > 0) score = 400; else if (title === normalizedQuery) score = 350; else if (title.includes(normalizedQuery)) score = 300; else if (tags.includes(normalizedQuery)) score = 250; else if (tags.some((tag) => tag.includes(normalizedQuery))) score = 200; else if (summary.includes(normalizedQuery)) score = 100; return { score, matchedIngredients }; } export function searchRecipes(recipes, query) { if (!normalizeSearchText(query)) { return recipes.map((recipe) => ({ recipe, score: 0, matchedIngredients: [] })); } return recipes .map((recipe, index) => ({ recipe, index, ...rankRecipeMatch(recipe, query) })) .filter(({ score }) => score > 0) .sort((a, b) => b.score - a.score || a.index - b.index) .map(({ recipe, score, matchedIngredients }) => ({ recipe, score, matchedIngredients })); }