feat: SMWS Search results

This commit is contained in:
Brennan Wilkes (Text Groove) 2026-01-24 11:03:06 -08:00
parent 140b73af37
commit 13c82a74ba
2 changed files with 220 additions and 1 deletions

View file

@ -3,6 +3,7 @@ import { tokenizeQuery, matchesAllTokens, displaySku, keySkuForRow, parsePriceTo
import { loadIndex, loadRecent, loadSavedQuery, saveQuery } from "./state.js";
import { aggregateBySku } from "./catalog.js";
import { loadSkuRules } from "./mapping.js";
import { smwsDistilleryCodesForQueryPrefix, smwsDistilleryCodeFromName } from "./smws.js";
export function renderSearch($app) {
$app.innerHTML = `
@ -387,7 +388,28 @@ export function renderSearch($app) {
if (!tokens.length) return;
const matches = allAgg.filter((it) => matchesAllTokens(it.searchText, tokens));
renderAggregates(matches);
// If query prefixes an SMWS distillery name, also surface SMWS bottles by code XXX.YYY where XXX matches.
const wantCodes = new Set(smwsDistilleryCodesForQueryPrefix($q.value));
if (!wantCodes.size) {
renderAggregates(matches);
return;
}
const seen = new Set(matches.map((it) => String(it?.sku || "")));
const extra = [];
for (const it of allAgg) {
const sku = String(it?.sku || "");
if (!sku || seen.has(sku)) continue;
const dCode = smwsDistilleryCodeFromName(it?.name || "");
if (dCode && wantCodes.has(String(dCode))) {
extra.push(it);
seen.add(sku);
}
}
// Put SMWS distillery matches first, then normal search matches.
renderAggregates([...extra, ...matches]);
}
$results.innerHTML = `<div class="small">Loading index…</div>`;

197
viz/app/smws.js Normal file
View file

@ -0,0 +1,197 @@
// viz/app/smws.js
import { normSearchText } from "./sku.js";
const DISTILLERIES = [
{ code: "1", name: "Glenfarclas" },
{ code: "2", name: "Glenlivet" },
{ code: "3", name: "Bowmore" },
{ code: "4", name: "Highland Park" },
{ code: "5", name: "Auchentoshan" },
{ code: "6", name: "Macduff" },
{ code: "7", name: "Longmorn" },
{ code: "8", name: "Tamdhu" },
{ code: "9", name: "Glen Grant" },
{ code: "10", name: "Bunnahabhain" },
{ code: "11", name: "Tomatin" },
{ code: "12", name: "BenRiach" },
{ code: "13", name: "Dalmore" },
{ code: "14", name: "Talisker" },
{ code: "15", name: "Glenfiddich" },
{ code: "16", name: "Glenturret" },
{ code: "17", name: "Scapa" },
{ code: "18", name: "Inchgower" },
{ code: "19", name: "Glen Garioch" },
{ code: "20", name: "Inverleven" },
{ code: "21", name: "Glenglassaugh" },
{ code: "22", name: "Glenkinchie" },
{ code: "23", name: "Bruichladdich" },
{ code: "24", name: "Macallan" },
{ code: "25", name: "Rosebank" },
{ code: "26", name: "Clynelish" },
{ code: "27", name: "Springbank" },
{ code: "28", name: "Tullibardine" },
{ code: "29", name: "Laphroaig" },
{ code: "30", name: "Glenrothes" },
{ code: "31", name: "Isle of Jura" },
{ code: "32", name: "Edradour" },
{ code: "33", name: "Ardbeg" },
{ code: "34", name: "Tamnavulin" },
{ code: "35", name: "Glen Moray" },
{ code: "36", name: "Benrinnes" },
{ code: "37", name: "Cragganmore" },
{ code: "38", name: "Caperdonich" },
{ code: "39", name: "Linkwood" },
{ code: "40", name: "Balvenie" },
{ code: "41", name: "Dailuaine" },
{ code: "42", name: "Tobermory" },
{ code: "43", name: "Port Ellen" },
{ code: "44", name: "Craigellachie" },
{ code: "45", name: "Dallas Dhu" },
{ code: "46", name: "Glenlossie" },
{ code: "47", name: "Benromach" },
{ code: "48", name: "Balmenach" },
{ code: "49", name: "St. Magdalene" },
{ code: "50", name: "Bladnoch" },
{ code: "51", name: "Bushmills" },
{ code: "52", name: "Old Pulteney" },
{ code: "53", name: "Caol Ila" },
{ code: "54", name: "Aberlour" },
{ code: "55", name: "Royal Brackla" },
{ code: "56", name: "Coleburn" },
{ code: "57", name: "Glen Mhor" },
{ code: "58", name: "Strathisla" },
{ code: "59", name: "Teaninich" },
{ code: "60", name: "Aberfeldy" },
{ code: "61", name: "Brora" },
{ code: "62", name: "Glenlochy" },
{ code: "63", name: "Glentauchers" },
{ code: "64", name: "Mannochmore" },
{ code: "65", name: "Imperial" },
{ code: "66", name: "Ardmore" },
{ code: "67", name: "Banff" },
{ code: "68", name: "Blair Athol" },
{ code: "69", name: "Glen Albyn" },
{ code: "70", name: "Balblair" },
{ code: "71", name: "Glenburgie" },
{ code: "72", name: "Miltonduff" },
{ code: "73", name: "Aultmore" },
{ code: "74", name: "North Port" },
{ code: "75", name: "Glenury / Glenury Royal" },
{ code: "76", name: "Mortlach" },
{ code: "77", name: "Glen Ord" },
{ code: "78", name: "Ben Nevis" },
{ code: "79", name: "Deanston" },
{ code: "80", name: "Glen Spey" },
{ code: "81", name: "Glen Keith" },
{ code: "82", name: "Glencadam" },
{ code: "83", name: "Convalmore" },
{ code: "84", name: "Glendullan" },
{ code: "85", name: "Glen Elgin" },
{ code: "86", name: "Glenesk" },
{ code: "87", name: "Millburn" },
{ code: "88", name: "Speyburn" },
{ code: "89", name: "Tomintoul" },
{ code: "90", name: "Pittyvaich" },
{ code: "91", name: "Dufftown" },
{ code: "92", name: "Lochside" },
{ code: "93", name: "Glen Scotia" },
{ code: "94", name: "Fettercairn" },
{ code: "95", name: "Auchroisk" },
{ code: "96", name: "GlenDronach" },
{ code: "97", name: "Littlemill" },
{ code: "98", name: "Inverleven" },
{ code: "99", name: "Glenugie" },
{ code: "100", name: "Strathmill" },
{ code: "101", name: "Knockando" },
{ code: "102", name: "Dalwhinnie" },
{ code: "103", name: "Royal Lochnagar" },
{ code: "104", name: "Glenburgie (Glencraig)" },
{ code: "105", name: "Tormore" },
{ code: "106", name: "Cardhu" },
{ code: "107", name: "Glenallachie" },
{ code: "108", name: "Allt-a-Bhainne" },
{ code: "109", name: "Miltonduff (Mosstowie)" },
{ code: "110", name: "Oban" },
{ code: "111", name: "Lagavulin" },
{ code: "112", name: "Loch Lomond (Inchmurrin / Inchmoan)" },
{ code: "113", name: "Braeval (Braes of Glenlivet)" },
{ code: "114", name: "Springbank (Longrow)" },
{ code: "115", name: "Knockdhu (AnCnoc)" },
{ code: "116", name: "Yoichi" },
{ code: "117", name: "Cooley (Unpeated)" },
{ code: "118", name: "Cooley / Connemara (Peated)" },
{ code: "119", name: "Yamazaki" },
{ code: "120", name: "Hakushu" },
{ code: "121", name: "Isle of Arran" },
{ code: "122", name: "Loch Lomond (Croftengea)" },
{ code: "123", name: "Glengoyne" },
{ code: "124", name: "Miyagikyo" },
{ code: "125", name: "Glenmorangie" },
{ code: "126", name: "Springbank (Hazelburn)" },
{ code: "127", name: "Bruichladdich (Port Charlotte)" },
{ code: "128", name: "Penderyn" },
{ code: "129", name: "Kilchoman" },
{ code: "130", name: "Chichibu" },
{ code: "131", name: "Hanyu" },
{ code: "132", name: "Karuizawa" },
{ code: "133", name: "Westland" },
{ code: "134", name: "Paul John" },
{ code: "135", name: "Loch Lomond" },
{ code: "136", name: "Eden Mill" },
{ code: "137", name: "St. Georges (The English Whisky Co.)" },
{ code: "138", name: "Nantou" },
{ code: "139", name: "Kavalan" },
{ code: "140", name: "Balcones" },
{ code: "141", name: "Fary Lochan" },
{ code: "142", name: "Breuckelen Distilling" },
{ code: "143", name: "Copperworks Distilling Co." },
{ code: "144", name: "High Coast Distillery" },
{ code: "145", name: "Smögen Whisky" },
{ code: "146", name: "Cotswolds" },
{ code: "147", name: "Archie Rose" },
{ code: "148", name: "Starward" },
{ code: "149", name: "Ardnamurchan" },
{ code: "150", name: "West Cork Distillers" },
{ code: "151", name: "Mackmyra" },
{ code: "152", name: "Shelter Point" },
{ code: "153", name: "Thy Whisky" },
{ code: "154", name: "Mosgaard Whisky" },
{ code: "155", name: "Milk & Honey Distillery" },
{ code: "156", name: "Glasgow Distillery" },
{ code: "157", name: "Distillerie de Warenghem" },
{ code: "158", name: "Yuza Distillery" },
{ code: "159", name: "Mars Shinshu" },
{ code: "160", name: "Mars Tsunuki" },
{ code: "161", name: "Nc'nean Distillery" },
{ code: "162", name: "Isle of Raasay" },
{ code: "163", name: "Isle of Harris Distillery" },
{ code: "164", name: "Penderyn" },
{ code: "165", name: "Wolfburn" },
];
const DIST = DISTILLERIES.map((d) => ({
code: String(d.code),
nameNorm: normSearchText(d.name),
}));
const SMWS_WORD_RE = /\bsmws\b/i;
const SMWS_CODE_RE = /\b(\d{1,3})\.(\d{1,4})\b/;
export function smwsDistilleryCodesForQueryPrefix(qRaw) {
const q = normSearchText(qRaw);
if (!q || q.length < 2) return [];
const out = new Set();
for (const d of DIST) {
if (d.nameNorm.startsWith(q)) out.add(d.code);
}
return Array.from(out);
}
// If a listing name contains "SMWS" and an SMWS code like 35.123, returns "35" (distillery code).
export function smwsDistilleryCodeFromName(name) {
const s = String(name || "");
if (!SMWS_WORD_RE.test(s)) return "";
const m = s.match(SMWS_CODE_RE);
return m ? String(m[1] || "") : "";
}