/** * Lightweight keyword-based aisle guesser for shopping list items. * * This is intentionally NOT an exhaustive ingredient database — just a few dozen * common keyword -> category mappings covering typical recipe ingredients, so * items generated from a meal plan (which never have an explicit `aisle` set * today) land in a reasonable category instead of an undifferentiated "Other" * bucket. When nothing matches, returns `null` and the caller falls back to * "Other" as before. * * Only ever used as a FALLBACK when an item doesn't already have an explicit * `aisle` — never overrides a user- or API-provided value. */ export const GROCERY_CATEGORIES = [ "Produce", "Dairy & Eggs", "Meat & Seafood", "Bakery", "Frozen", "Pantry", "Spices & Condiments", "Beverages", ] as const; export type GroceryCategory = (typeof GROCERY_CATEGORIES)[number]; // Ordered map of category -> keywords. Checked in order, first match wins, so // more specific keywords should generally come before more generic ones. const CATEGORY_KEYWORDS: [GroceryCategory, string[]][] = [ ["Produce", [ "lettuce", "spinach", "kale", "arugula", "cabbage", "carrot", "celery", "onion", "garlic", "shallot", "scallion", "leek", "potato", "sweet potato", "tomato", "cucumber", "zucchini", "squash", "pepper", "chili", "chile", "broccoli", "cauliflower", "mushroom", "avocado", "lemon", "lime", "orange", "apple", "banana", "berry", "berries", "grape", "melon", "peach", "pear", "plum", "mango", "pineapple", "cilantro", "parsley", "basil", "mint", "dill", "thyme", "rosemary", "ginger", "corn", "peas", "beans", "asparagus", "radish", "beet", "fennel", "herb", "greens", ]], ["Dairy & Eggs", [ "milk", "cream", "yogurt", "yoghurt", "butter", "cheese", "egg", "eggs", "sour cream", "cottage cheese", "mascarpone", "ricotta", "buttermilk", "half and half", "creme fraiche", ]], ["Meat & Seafood", [ "chicken", "beef", "pork", "lamb", "turkey", "bacon", "sausage", "ham", "steak", "ground beef", "mince", "salmon", "tuna", "shrimp", "prawn", "cod", "tilapia", "fish", "crab", "lobster", "scallop", "mussel", "clam", "chorizo", "prosciutto", "duck", ]], ["Bakery", [ "bread", "baguette", "roll", "bun", "bagel", "tortilla", "pita", "naan", "croissant", "muffin", "brioche", "loaf", ]], ["Frozen", [ "frozen", "ice cream", "popsicle", "frozen peas", "frozen berries", ]], ["Beverages", [ "juice", "soda", "water", "coffee", "tea", "wine", "beer", "sparkling", "kombucha", "cider", ]], ["Spices & Condiments", [ "salt", "pepper flakes", "cumin", "paprika", "cinnamon", "nutmeg", "oregano", "turmeric", "cayenne", "curry powder", "chili powder", "spice", "vanilla", "ketchup", "mustard", "mayo", "mayonnaise", "soy sauce", "hot sauce", "vinegar", "olive oil", "vegetable oil", "sesame oil", "honey", "maple syrup", "jam", "sauce", "dressing", "salsa", ]], ["Pantry", [ "flour", "sugar", "rice", "pasta", "noodle", "spaghetti", "quinoa", "oats", "oatmeal", "cereal", "beans", "lentil", "chickpea", "canned", "stock", "broth", "bouillon", "yeast", "baking powder", "baking soda", "cornstarch", "breadcrumb", "nut", "almond", "walnut", "peanut", "cashew", "chocolate", "cocoa", "coconut milk", "tomato paste", "tomato sauce", "crushed tomato", ]], ]; /** * Guesses a grocery aisle/category from a raw ingredient name via simple * keyword matching. Returns `null` when nothing matches (caller should fall * back to "Other"). */ export function guessAisle(rawName: string): GroceryCategory | null { const name = rawName.toLowerCase().trim(); if (!name) return null; for (const [category, keywords] of CATEGORY_KEYWORDS) { for (const keyword of keywords) { if (name.includes(keyword)) return category; } } return null; }