1/*! 2 * BroadbandSwitch.uk | site-search.js v2 (29 September 2026) 3 * ---------------------------------------------------------------- 4 * One file, two jobs, same engine. 5 * 6 * 1. Header overlay on every page. Opens from any [data-site-search-open] 7 * trigger, from any header link to /sitemap.html, from the "/" key and 8 * from Ctrl/Cmd+K. Ranked results as you type. 9 * 2. On /sitemap.html it drives the on-page search: filters the server-rendered 10 * list, adds section filters, an A to Z for the provider directory, and reads 11 * ?q= from the URL. 12 * 13 * New in v2 14 * - Typo tolerance. A word the site never uses is corrected outright 15 * ("brodband speed tset" shows results for "broadband speed test", with a 16 * link to search as typed). A plausible word with a better neighbour is 17 * offered as "Did you mean ...?". Provider names are corrected as a phrase 18 * ("virgin meida" -> "virgin media"). 19 * - Intent cards. A postcode opens the comparison tool for that postcode; a 20 * provider name shows its deals page, directory profile and head-to-heads; a 21 * town shows its local deals page. 22 * - Rare words outrank common ones ("hyperoptic deals" ranks Hyperoptic pages 23 * above generic deals pages). 24 * - Stop words are ignored ("cancel my contract"). 25 * - Recent searches (this browser only) and popular searches on an empty box. 26 * - Section filters, counts and an A to Z on the browse page. 27 * - Styles for the new pieces are injected by this file, so a cached copy of 28 * site-search.css can never leave them unstyled. 29 * 30 * Progressive enhancement throughout. If the index cannot load, every entry 31 * point still lands on /sitemap.html?q=... and the static list is fully usable 32 * with a plain substring filter. 33 * 34 * No dependencies. Safe to load with defer. 35 */ 36(function () { 37 'use strict'; 38 39 var INDEX_URL = '/search-index.json'; 40 var SEARCH_PAGE = '/sitemap.html'; 41 var CACHE_KEY = 'bsu:search-index:v2'; 42 var RECENT_KEY = 'bsu:search-recent:v1'; 43 var MAX_RESULTS = 60; 44 var MAX_OVERLAY = 8; 45 var POPULAR = ['BT vs Sky', 'social tariff', 'speed test', 'exit fees', 'full fibre', 'router login', 'cancel contract early', 'price rises 2026']; 46 47 var index = null; // prepared index 48 var loading = null; 49 var overlay = null; 50 var els = {}; 51 var activeIndex = -1; 52 var lastFocus = null; 53 var results = []; 54 55 var reduceMotion = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false; 56 57 /* ---------------------------------------------------------------- * 58 * Language helpers 59 * ---------------------------------------------------------------- */ 60 61 /* Customer language to site language. Each key expands into extra tokens that also count as a match. */ 62 var SYN = { 63 change: ['switch'], changing: ['switch'], swap: ['switch'], swapping: ['switch'], move: ['switch', 'moving'], 64 leave: ['switch', 'exit', 'leave'], leaving: ['switch', 'exit'], cancel: ['exit', 'cancel', 'cooling', 'early'], 65 cancelling: ['exit', 'cooling', 'early'], quit: ['switch', 'exit'], internet: ['broadband'], 66 wifi: ['wi-fi', 'wifi', 'router'], 'wi-fi': ['wifi', 'router'], cheap: ['cheapest', 'under', 'value', 'cheap'], 67 affordable: ['cheapest', 'under', 'social'], budget: ['cheapest', 'under'], price: ['cost', 'price', 'rise'], 68 prices: ['cost', 'price', 'rises'], bill: ['cost', 'price', 'bill'], bills: ['cost', 'price', 'rises'], 69 supplier: ['provider'], company: ['provider'], isp: ['provider', 'providers', 'isp'], fast: ['speed', 'gigabit', 'fast'], 70 quick: ['speed'], slow: ['speed', 'slow', 'slower'], elderly: ['pensioners', 'older'], oap: ['pensioners'], 71 retired: ['pensioners'], tenant: ['renters', 'rented', 'renting'], renting: ['renters', 'rented'], rent: ['renters', 'rented'], 72 television: ['tv'], feedback: ['contact', 'corrections'], complaint: ['complaint', 'complaints', 'compensation', 'ombudsman'], 73 complain: ['complaint', 'complaints', 'compensation'], help: ['help', 'contact', 'guide'], deal: ['deals'], offer: ['deal
73s'], 74 offers: ['deals'], fibre: ['fibre', 'fttp', 'full'], fiber: ['fibre', 'fttp'], gig: ['gigabit'], speeds: ['speed'], 75 drops: ['drop', 'dropping', 'drops'], dropping: ['drop', 'drops'], buffering: ['bufferbloat', 'slow', 'speed'], 76 lag: ['latency', 'ping'], gaming: ['gaming', 'latency', 'ping'], kids: ['children', 'parental', 'online safety'], 77 children: ['kids', 'parental'], student: ['students'], pensioner: ['pensioners'], landlord: ['renters', 'rented', 'tenant'], 78 ots: ['one touch switch'], pstn: ['landline', 'switch-off'], router: ['router', 'wi-fi'] 79 }; 80 var STOP = {}; 81 'a an the my me i is it to for in of on and or do can how what why with at be am are was get got from about into vs v your you we our this that does did should will would need'.split(' ').forEach(function (w) { STOP[w] = true; }); 82 83 function norm(s) { 84 return String(s || '').toLowerCase() 85 .normalize('NFD').replace(/[Ì-ͯ]/g, '') 86 .replace(/[^a-z0-9£%+ -]+/g, ' ').replace(/-/g, ' ') 87 .replace(/\s+/g, ' ').trim(); 88 }
89 function toksAll(s) { return norm(s).split(' ').filter(Boolean); } 90 function toks(s) { 91 var t = toksAll(s), k = t.filter(function (w) { return !STOP[w]; }); 92 return k.length ? k : t; 93 } 94 function expand(t) { 95 var out = [t]; 96 (SYN[t] || []).forEach(function (x) { x.split(' ').forEach(function (y) { out.push(y); }); }); 97 return out; 98 } 99 function escapeHtml(s) { 100 return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); 101 } 102 function trim(s, n) { s = String(s); return s.length <= n ? s : s.slice(0, n - 1).replace(/\s+\S*$/, '') + 'â¦'; } 103 function highlight(text, q) { 104 var safe = escapeHtml(text); 105 var t = toks(q).filter(function (x) { return x.length > 1; }); 106 if (!t.length) return safe; 107 try { 108 var rx = new RegExp('(' + t.map(function (x) { return x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }).join('|') + ')', 'gi'); 109 return safe.replace(rx, '<mark>$1</mark>'); 110 } catch (e) { return safe; } 111 } 112 113 /* ---------------------------------------------------------------- * 114 * Index loading and preparation 115 * ---------------------------------------------------------------- */ 116 117 function loadIndex() { 118 if (index) return Promise.resolve(index); 119 if (loading) return loading; 120 try { 121 var cached = sessionStorage.getItem(CACHE_KEY); 122 if (cached) { index = prepare(JSON.parse(cached)); return Promise.resolve(index); } 123 } catch (e) { /* storage unavailable */ } 124 loading = fetch(INDEX_URL, { credentials: 'same-origin' }) 125 .then(function (r) { if (!r.ok) throw new Error('index ' + r.status); return r.json(); }) 126 .then(function (data) { 127 try { sessionStorage.setItem(CACHE_KEY, JSON.stringify(data)); } catch (e) { /* ignore */ } 128 index = prepare(data); 129 return index; 130 }) 131 .catch(function () { index = null; loading = null; return null; }); 132 return loading; 133 } 134 135 function prepare(data) { 136 if (!data || !data.pages) return null; 137 var vocab = {}; 138 var providers = []; 139 data.pages.forEach(function (p, i) { 140 p.i = i; 141 var slug = p.u.replace(/[/.]/g, ' ').replace(/-/g, ' '); 142 p._t = norm(p.t); 143 p._h = norm([p.t, p.d || '', p.h || '', p.k || '', p.s, slug, (p.a || []).join(' ')].join(' ')); 144 p._tt = {}; 145 toksAll(p.t).forEach(function (w) { p._tt[w] = true; }); 146 var seen = {}; 147 toksAll(p._h).forEach(function (w) { if (w.length > 2 && !seen[w]) { seen[w] = true; vocab[w] = (vocab[w] || 0) + 1; } }); 148 if (p.s === 'Provider directory') providers.push({ name: p.t.split(':')[0].trim(), norm: norm(p.t.split(':')[0]), page: p }); 149 }); 150 data._vocab = vocab; 151 data._vocabList = Object.keys(vocab).map(function (w) { return { w: w, f: vocab[w], l: w.length }; }); 152 data._providers = providers; 153 data._deals = data.pages.filter(function (p) { return p.s === 'Providers & head-to-heads' && /-broadband-deals\.html$/.test(p.u) && !/-vs-/.test(p.u); }); 154 data._h2h = data.pages.filter(function (p) { return /-vs-/.test(p.u); }); 155 data._brands = data._deals.map(function (d) { 156 var name = d.t.replace(/\s+broadband deals.*$/i, '').replace(/\s+deals.*$/i, '').replace(/:.*$/, '').trim(); 157 return { name: name, norm: norm(name) }; 158 }); 159 data._brands.forEach(function (b) { 160 var f = b.norm.split(' ')[0]; 161 b.unique = data._brands.filter(function (x) { return x.norm.split(' ')[0] === f; }).length === 1 && f.length >= 2; 162 }); 163 data._locations = data.pages.filter(function (p) { return p.s === 'Locations' && /-broadband-deals\.html$/.test(p.u); }) 164 .map(function (p) { return { town: norm(p.t.replace(/broadband deals.*/i, '').trim()), page: p }; }); 165 return data; 166 } 167 168 /* ---------------------------------------------------------------- * 169 * Typo tolerance: bounded Damerau-Levenshtein against the site vocabulary 170 * ---------------------------------------------------------------- */ 171 172 function dl(a, b, max) { 173 var la = a.length, lb = b.length; 174 if (Math.abs(la - lb) > max) return max + 1; 175 var d = [], i, j; 176 for (i = 0; i <= la; i++) { d[i] = [i]; } 177 for (j = 1; j <= lb; j++) d[0][j] = j; 178 for (i = 1; i <= la; i++) { 179 var rowMin = Infinity; 180 for (j = 1; j <= lb; j++) { 181 var cost = a[i - 1] === b[j - 1] ? 0 : 1; 182 var v = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost); 183 if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) v = Math.min(v, d[i - 2][j - 2] + 1); 184 d[i][j] = v; if (v < rowMin) rowMin = v; 185 } 186 if (rowMin > max) return max + 1; 187 } 188 return d[la][lb]; 189 } 190 function maxEdits(w) { return w.length <= 3 ? 0 : w.length <= 5 ? 1 : w.length <= 9 ? 2 : 3; } 191 function known(t) { 192 if (!index) return true; 193 if (index._vocab[t]) return true; 194 if (t.length >= 4) { for (var i = 0; i < index._vocabList.length; i++) if (index._vocabList[i].w.indexOf(t) === 0) return true; } 195 return false;
196 } 197 function correctToken(t) { 198 if (!index || /^\d/.test(t) || t.length < 3) return null; 199 var max = maxEdits(t); 200 if (!max) return null; 201 var best = null; 202 for (var i = 0; i < index._vocabList.length; i++) { 203 var v = index._vocabList[i]; 204 if (Math.abs(v.l - t.length) > max) continue; 205 var e = dl(t, v.w, max); 206 if (e > max) continue; 207 var score = e * 10 - Math.log(v.f + 1) - (v.w[0] === t[0] ? 2 : 0); 208 if (!best || score < best.score) best = { w: v.w, e: e, score: score }; 209 } 210 return best ? best.w : null; 211 } 212 function correctPhrase(q) { 213 if (!index) return null; 214 var nq = norm(q); 215 if (nq.length < 4) return null; 216 var best = null, max = maxEdits(nq); 217 for (var i = 0; i < index._providers.length; i++) { 218 var pr = index._providers[i]; 219 if (Math.abs(pr.norm.length - nq.length) > max + 1) continue; 220 var e = dl(nq, pr.norm, max + 1); 221 if (e <= max + 1 && e > 0 && (!best || e < best.e)) best = { w: pr.norm, e: e }; 222 } 223 return best ? best.w : null; 224 } 225 function suggest(q) { 226 var t = toks(q); 227 if (!t.length) return null; 228 var fixed = t.map(function (w) { return known(w) ? w : (correctToken(w) || w); }); 229 if (fixed.join(' ') !== t.join(' ')) return fixed.join(' '); 230 var pc = correctPhrase(q); 231 if (pc && pc !== norm(q)) return pc; 232 return null; 233 } 234 235 /* ---------------------------------------------------------------- * 236 * Ranking 237 * ---------------------------------------------------------------- */ 238 239 function rarity(t) { 240 var df = index._vocab[t] || 0; 241 return df ? 1 + Math.max(0, 2.6 - Math.log(df + 1) / Math.LN10) : 1.6; 242 } 243 function titleHasPrefix(p, a) { 244 if (a.length <= 3) return false; 245 for (var w in p._tt) if (w.indexOf(a) === 0) return true; 246 return false; 247 } 248 function search(rawQuery) { 249 if (!index) return []; 250 var nq = norm(rawQuery); 251 if (!nq) return []; 252 var tokens = toks(rawQuery); 253 var out = []; 254 for (var i = 0; i < index.pages.length; i++) { 255 var p = index.pages[i]; 256 var score = 0; 257 if (p._t === nq) score += 1000; 258 else if (p._t.indexOf(nq) === 0) score += 400;
259 else if (p._t.indexOf(nq) !== -1) score += 250; 260 if (p.a && p.a.some(function (a) { return norm(a).indexOf(nq) !== -1; })) score += 220; 261 var inTitle = 0, inHay = 0, tW = 0, hW = 0; 262 for (var t = 0; t < tokens.length; t++) { 263 var alts = expand(tokens[t]); 264 var r = rarity(tokens[t]); 265 var hitT = false, hitP = false, hitH = false; 266 for (var k = 0; k < alts.length; k++) { 267 if (p._tt[alts[k]]) hitT = true; 268 else if (!hitT && titleHasPrefix(p, alts[k])) hitP = true; 269 if (p._h.indexOf(alts[k]) !== -1) hitH = true; 270 } 271 if (hitT) { inTitle++; tW += r; } else if (hitP) { inTitle += 0.7; tW += r * 0.7; } 272 if (hitH) { inHay++; hW += r; } 273 } 274 if (inHay === 0 && score === 0) continue; 275 score += (inTitle >= tokens.length ? 160 : 0) + tW * 40; 276 score += (inHay >= tokens.length ? 60 : 0) + hW * 12; 277 if (inHay < tokens.length && tokens.length > 1 && inTitle < 1) continue; /* every word must land somewhere */ 278 score += (p.w || 1) * 10; 279 if (p.s === 'Provider directory') score -= 25; /* profiles rank below guides unless named */ 280 out.push({ p: p, score: score }); 281 } 282 out.sort(function (a, b) { 283 if (b.score !== a.score) return b.score - a.score; 284 if ((b.p.w || 1) !== (a.p.w || 1)) return (b.p.w || 1) - (a.p.w || 1); 285 return a.p.t.localeCompare(b.p.t, 'en-GB'); 286 }); 287 return out.map(function (r) { return r.p; }); 288 } 289 290 /* resolve: returns {results, shownQuery, dym, mode} where mode is exact | auto | suggest */ 291 function resolve(rawQ) { 292 var q = rawQ.trim(); 293 if (!q) return { results: [], shownQuery: '', dym: null, mode: 'exact' }; 294 var primary = search(q); 295 var alt = suggest(q); 296 if (alt) { 297 var altRes = search(alt); 298 var unknown = toks(q).some(function (w) { return !known(w) && !/^\d/.test(w) && w.length > 2; }); 299 if (altRes.length && (unknown || primary.length < 3)) return { results: altRes, shownQuery: alt, dym: q, mode: 'auto' }; 300 if (altRes.length > primary.length) return { results: primary, shownQuery: q, dym: alt, mode: 'suggest' }; 301 } 302 return { results: primary, shownQuery: q, dym: null, mode: 'exact' }; 303 } 304 305 /* ---------------------------------------------------------------- * 306 * Intent cards: postcode, provider, town 307 * ---------------------------------------------------------------- */ 308 309 var POSTCODE = /^[a-z]{1,2}\d[a-z\d]?\s*\d?[a-z]{0,2}$/i; 310 var ICONS = { 311 postcode: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 10c0 6-8 11-8 11S4 16 4 10a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>', 312 provider: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 10h18M8 15h4"/></svg>', 313 town: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 21h18M5 21V7l7-4 7 4v14M9 21v-6h6v6"/></svg>' 314 }; 315 function intents(q) { 316 if (!index) return []; 317 var cards = []; 318 var raw = q.trim(); 319 var nq = norm(q); 320 if (POSTCODE.test(raw) && raw.replace(/\s/g, '').length >= 5) { 321 var pc = raw.toUpperCase().replace(/\s+/g, ' '); 322 cards.push({ kind: 'postcode', title: 'Compare live deals at ' + pc, sub: 'Every provider and price available at your address, cheapest first.', href: '/compare/?postcode=' + encodeURIComponent(pc), cta: 'Check this postcode' }); 323 return cards; 324 } 325 var loc = null; 326 for (var i = 0; i < index._locations.length; i++) { 327 var l = index._locations[i]; 328 if (l.town === nq || (nq.length >= 4 && l.town.indexOf(nq) === 0 && l.town.split(' ')[0] === nq)) { loc = l; break; } 329 } 330 if (loc) { 331 cards.push({ kind: 'town', title: 'Broadband deals in ' + loc.page.t.replace(/ broadband deals.*$/i, ''), sub: 'Providers, speeds and prices available locally, then check your exact postcode.', href: loc.page.u, cta: 'See local deals' }); 332 return cards; 333 } 334 var core = nq.replace(/\b(deals?|broadband|review|reviews|offers?|prices?|internet|fibre)\b/g, ' ').replace(/\s+/g, ' ').trim(); 335 var brand = null; 336 if (core) { 337 for (var b = 0; b < index._brands.length; b++) { 338 var br = index._brands[b]; 339 if (br.norm === core || br.norm === core + ' broadband' || (core.length >= 3 && br.norm.split(' ')[0] === core && br.unique)) { brand = br; break; } 340 } 341 } 342 var pr = null; 343 for (var j = 0; j < index._providers.length; j++) { 344 var x = index._providers[j]; 345 if (x.norm === nq || x.norm === core) { pr = x; break; } 346 } 347 if (!pr && brand) { 348 for (var k = 0; k < index._providers.length; k++) { 349 var y = index._providers[k]; 350 if (y.norm === brand.norm || y.norm.split(' ')[0] === brand.norm.split(' ')[0]) { pr = y; break; } 351 } 352 } 353 if (pr || brand) { 354 var name = pr ? pr.name : brand.name; 355 var first = norm(name).split(' ')[0]; 356 var deal = null; 357 for (var d = 0; d < index._deals.length; d++) { var dd = index._deals[d]; if (norm(dd.t).indexOf(first) === 0 || norm(dd.u).indexOf('/' + first + ' ') !== -1 || dd.u.indexOf('/' + first + '-') === 0) { deal = dd; break; } } 358 var h2h = index._h2h.filter(function (h) { return norm(h.t).split(' ').indexOf(first) !== -1; }).slice(0, 3); 359 var profile = pr ? pr.page : null; 360 var links = []; 361 if (deal && profile) links.push({ t: 'Directory profile', u: profile.u }); 362 h2h.forEach(function (h) { links.push({ t: h.t.replace(/[:.].*$/, '').trim(), u: h.u }); }); 363 cards.push({ kind: 'provider', title: name, sub: deal ? "This month's deals, the real 24-month cost and the directory profile." : 'Directory profile: contact details, coverage, verification and Trustpilot rating.', href: deal ? deal.u : (profile ? profile.u : '/broadband-providers.php'), cta: deal ? 'See deals' : 'Open profile', links: links }); 364 } 365 return cards; 366 } 367 function renderCards(cards) { 368 return cards.map(function (c) { 369 return '<div class="bsu-card"><div class="bsu-card__ic">' + ICONS[c.kind] + '</div><div class="bsu-card__body"><b>' + escapeHtml(c.title) + '</b><span>' + escapeHtml(c.sub) + '</span>' + 370 (c.links && c.links.length ? '<div class="bsu-card__links">' + c.links.map(function (l) { return '<a href="' + escapeHtml(l.u) + '">' + escapeHtml(l.t) + '</a>'; }).join('') + '</div>' : '') + 371 '</div><a class="bsu-card__go" href="' + escapeHtml(c.href) + '">' + escapeHtml(c.cta) + '</a></div>'; 372 }).join(''); 373 } 374 375 /* ---------------------------------------------------------------- * 376 * Recent searches (per browser), analytics 377 * ---------------------------------------------------------------- */ 378 379 function getRecent() { try { return JSON.parse(localStorage.getItem(RECENT_KEY) || '[]'); } catch (e) { return []; } } 380 function addRecent(q) { try { var r = getRecent().filter(function (x) { return x !== q; }); r.unshift(q); localStorage.setItem(RECENT_KEY, JSON.stringify(r.slice(0, 6))); } catch (e) { /* ignore */ } } 381 function track(event, q, extra) { 382 try { 383 var payload = { event: event, search_term: q }; 384 if (extra) for (var k in extra) payload[k] = extra[k]; 385 if (window.dataLayer) window.dataLayer.push(payload); 386 else if (typeof window.gtag === 'function') window.gtag('event', event, payload); 387 } catch (e) { /* analytics must never break search */ } 388 } 389 390 /* ---------------------------------------------------------------- * 391 * Styles for the v2 pieces (injected so a cached stylesheet cannot strand them) 392 * ---------------------------------------------------------------- */ 393 394 function injectStyles() { 395 if (document.getElementById('bsu-search-v2-css')) return; 396 var css = [ 397 '.bsu-search__dym,.sm-notice{display:flex;gap:.5rem;align-items:flex-start;margin:.625rem 1rem 0;padding:.625rem .75rem;border-radius:10px;background:#fef3e2;color:#1c1524;font-size:.875rem;line-height:1.45}', 398 '.sm-notice{margin:.75rem 0 0}', 399 '.bsu-search__dym[hidden],.sm-notice[hidden]{display:none}', 400 '.bsu-search__dym svg,.sm-notice svg{color:#b45309;flex:0 0 auto;margin-top:.1rem}', 401 '.bsu-search__dym a,.sm-notice a{color:var(--bsu-brand,var(--brand,#E50571));font-weight:600}', 402 '.sm-notice .sm-notice__sub{display:block;color:#5d5566;font-size:.8125rem;margin-top:.15rem}', 403 '.bsu-search__intent{margin:.625rem 1rem 0;display:grid;gap:.5rem}', 404 '.sm-intent{margin:.75rem 0 0;display:grid;gap:.625rem}', 405 '.bsu-card{border:1px solid var(--bsu-line,#e6e0ea);border-left:4px solid var(--bsu-brand,var(--brand,#E50571));border-radius:12px;padding:.75rem .875rem;background:var(--bsu-surface,#fff);display:flex;gap:.75rem;
405align-items:center;flex-wrap:wrap}', 406 '.bsu-card__ic{width:38px;height:38px;border-radius:10px;background:#fde7f1;display:grid;place-items:center;color:#b0045a;flex:0 0 auto}', 407 '.bsu-card__body{flex:1 1 200px;min-width:0}.bsu-card__body b{display:block;font-weight:600}.bsu-card__body span{color:#5d5566;font-size:.8125rem;display:block}', 408 '.bsu-card__links{display:flex;gap:.75rem;flex-wrap:wrap;font-size:.8125rem;margin-top:.25rem}.bsu-card__links a{color:var(--bsu-brand,var(--brand,#E50571));font-weight:600}', 409 '.bsu-card__go{background:var(--bsu-brand,var(--brand,#E50571));color:#fff !important;border-radius:999px;padding:.5rem 1rem;font-weight:600;font-size:.8125rem;text-decoration:none;white-space:nowrap}', 410 '.bsu-card__go:hover{background:#b0045a}', 411 '.bsu-search__start{padding:.25rem 1rem .875rem}.bsu-search__start h4{margin:.75rem 0 .375rem;font-size:.6875rem;letter-spacing:.09em;text-transform:uppercase;color:#8e8497;font-weight:600}', 412 '.bsu-chips{display:flex;flex-wrap:wrap;gap:.5rem}.bsu-chip{border:1px solid var(--bsu-line,#e6e0ea);background:var(--bsu-surface,#fff);border-radius:999px;padding:.3rem .75rem;font:inherit;font-size:.8125rem;color:#1c1524;cursor:pointer}.bsu-chip:hover{border-color:var(--bsu-brand,#E50571);color:#b0045a}', 413 '.sm-filters{display:flex;flex-wrap:wrap;gap:.5rem;margin:1rem 0 .25rem}.sm-filters[hidden]{display:none}', 414 '.sm-filters button{border:1px solid var(--bsu-line,#e6e0ea);background:var(--bsu-surface,#fff);border-radius:999px;padding:.3rem .75rem;font:inherit;font-size:.8125rem;color:#5d5566;cursor:pointer;display:inline-flex;gap:.4rem;align-items:center}', 415 '.sm-filters button b{font-weight:500;color:#8e8497;font-variant-numeric:tabular-nums}', 416 '.sm-filters button[aria-pressed="true"]{background:#2b1030;border-color:#2b1030;color:#fff}.sm-filters button[aria-pressed="true"] b{color:rgba(255,255,255,.7)}', 417 '.sm-az{display:flex;flex-wrap:wrap;gap:.25rem;margin:.5rem 0 .75rem}.sm-az button{border:0;background:#efeaf2;border-radius:6px;min-width:30px;height:28px;font:inherit;font-size:.8125rem;font-weight:500;color:#5d5566;cursor:pointer;padding:0 .4rem}.sm-az button[aria-pressed="true"]{background:var(--bsu-brand,#E50571);color:#fff}', 418 '.sm-recent{display:flex;flex-wrap:wrap;gap:.5rem .875rem;align-items:center;font-size:.8125rem;color:#5d5566;margin:.5rem 0 0}.sm-recent[hidden]{display:none}.sm-recent button{border:0;background:#efeaf2;border-radius:999px;padding:.25rem .7rem;font:inherit;font-size:.8125rem;color:#1c1524;cursor:pointer}', 419 '.sm-item mark{background:#fde7f1;color:inherit;border-radius:3px;padding:0 1px}', 420 '.sm-top{border:1px solid var(--bsu-line,#e6e0ea);border-radius:12px;padding:.75rem 1rem;background:var(--bsu-surface,#fff)}.sm-top h4{margin:0 0 .4rem;font-size:.6875rem;letter-spacing:.09em;text-transform:uppercase;color:#8e8497;font-weight:600}.sm-top ol{margin:0;padding-left:1.25rem;display:grid;gap:.3rem}.sm-top a{font-weight:600}.sm-top__s{display:block;font-size:.75rem;color:#8e8497}', 421 '.sm-group__title .sm-group__count{font-variant-numeric:tabular-nums}', 422 '.sm-clear{border:0;background:#efeaf2;border-radius:999px;width:28px;height:28px;display:grid;place-items:center;color:#5d5566;cursor:pointer;flex:0 0 auto}.sm-clear[hidden]{display:none}', 423 'input.sm-search__input::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none;display:none}', 424 '@media (prefers-color-scheme: dark){html[data-theme="dark"] .bsu-search__dym,html[data-theme="dark"] .sm-notice{background:#3a2a14;color:#f4eef7}html[data-theme="dark"] .bsu-card__body span,html[data-theme="dark"] .sm-notice .sm-notice__sub{color:#a99fb2}}' 425 ].join('\n'); 426 var st = document.createElement('style'); 427 st.id = 'bsu-search-v2-css'; 428 st.textContent = css; 429 document.head.appendChild(st); 430 } 431 432 /* ---------------------------------------------------------------- * 433 * Overlay 434 * ---------------------------------------------------------------- */ 435
436 var SVG_ICON = '<svg class="bsu-search__icon" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><circle cx="9" cy="9" r="6" fill="none" stroke="currentColor" stroke-width="2"/><line x1="13.5" y1="13.5" x2="18" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>'; 437 var SVG_REDO = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 3v6h-6"/></svg>'; 438 var SVG_INFO = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>'; 439 440 function buildOverlay() { 441 if (overlay) return overlay; 442 injectStyles(); 443 overlay = document.createElement('div'); 444 overlay.className = 'bsu-search'; 445 overlay.hidden = true; 446 overlay.setAttribute('role', 'dialog'); 447 overlay.setAttribute('aria-modal', 'true'); 448 overlay.setAttribute('aria-label', 'Search BroadbandSwitch.uk'); 449 if (reduceMotion) overlay.setAttribute('data-reduce-motion', 'true'); 450 overlay.innerHTML = 451 '<div class="bsu-search__backdrop" data-close></div>' + 452 '<div class="bsu-search__panel">' + 453 '<div class="bsu-search__bar">' + SVG_ICON + 454 '<input class="bsu-search__input" type="search" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" ' + 455 'placeholder="Search a provider, town, deal type or problem" aria-label="Search BroadbandSwitch.uk" ' + 456 'aria-controls="bsu-search-results" aria-expanded="false" role="combobox" aria-autocomplete="list">' + 457 '<button class="bsu-search__close" type="button" data-close aria-label="Close search">Esc</button>' + 458 '</div>' + 459 '<div class="bsu-search__status" role="status" aria-live="polite"></div>' + 460 '<div class="bsu-search__dym" hidden></div>' + 461 '<div class="bsu-search__intent"></div>' + 462 '<div class="bsu-search__start"></div>' + 463 '<ul class="bsu-search__results" id="bsu-search-results" role="listbox" aria-label="Search results"></ul>' + 464 '<div class="bsu-search__foot">' + 465 '<span><kbd>↑</kbd><kbd>↓</kbd> to move</span><span><kbd>Enter</kbd> to open</span><span><kbd>Esc</kbd> to close</span>' + 466 '<a class="bsu-search__all" href="' + SEARCH_PAGE + '">Browse every page</a>' + 467 '</div>' + 468 '</div>'; 469 document.body.appendChild(overlay); 470 els.input = overlay.querySelector('.bsu-search__input'); 471 els.list = overlay.querySelector('.bsu-search__results'); 472 els.status = overlay.querySelector('.bsu-search__status'); 473 els.dym = overlay.querySelector('.bsu-search__dym'); 474 els.intent = overlay.querySelector('.bsu-search__intent'); 475 els.start = overlay.querySelector('.bsu-search__start'); 476 els.all = overlay.querySelector('.bsu-search__all'); 477 overlay.addEventListener('click', function (e) { 478 if (e.target.closest('[data-close]')) { closeOverlay(); return; } 479 var chip = e.target.closest('[data-oq]'); 480 if (chip) { e.preventDefault(); els.input.value = chip.getAttribute('data-oq'); render(els.input.value); els.input.focus(); } 481 }); 482 els.input.addEventListener('input', function () { render(els.input.value); }); 483 els.input.addEventListener('keydown', onKeydown); 484 els.list.addEventListener('mousemove', function (e) { var li = e.target.closest('li[data-i]'); if (li) setActive(Number(li.getAttribute('data-i')), false); }); 485 return overlay; 486 } 487 488 function onKeydown(e) { 489 if (e.key === 'Escape') { e.preventDefault(); closeOverlay(); return; } 490 if (e.key === 'ArrowDown') { e.preventDefault(); if (results.length) setActive(activeIndex + 1 >= results.length ? 0 : activeIndex + 1, true); return; } 491 if (e.key === 'ArrowUp') { e.preventDefault(); if (results.length) setActive(activeIndex - 1 < 0 ? results.length - 1 : activeIndex - 1, true); return; } 492 if (e.key === 'Home' && results.length) { e.preventDefault(); setActive(0, true); return; } 493 if (e.key === 'End' && results.length) { e.preventDefault(); setActive(results.length - 1, true); return; } 494 if (e.key === 'Enter') { 495 e.preventDefault(); 496 var q = els.input.value.trim(); 497 if (activeIndex >= 0 && results[activeIndex]) { addRecent(q); window.location.href = results[activeIndex].u; } 498 else if (q) { addRecent(q); window.location.href = SEARCH_PAGE + '?q=' + encodeURIComponent(q); } 499 return; 500 } 501 if (e.key === 'Tab') trapFocus(e); 502 } 503 function trapFocus(e) { 504 var focusable = overlay.querySelectorAll('input, button, a[href]'); 505 if (!focusable.length) return;
506 var first = focusable[0], last = focusable[focusable.length - 1]; 507 if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } 508 else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } 509 } 510 511 function renderStart() { 512 var rec = getRecent(); 513 els.start.hidden = false; 514 els.start.innerHTML = 515 (rec.length ? '<h4>Recent</h4><div class="bsu-chips">' + rec.map(function (x) { return '<button type="button" class="bsu-chip" data-oq="' + escapeHtml(x) + '">' + escapeHtml(x) + '</button>'; }).join('') + '</div>' : '') + 516 '<h4>Popular right now</h4><div class="bsu-chips">' + POPULAR.map(function (x) { return '<button type="button" class="bsu-chip" data-oq="' + escapeHtml(x) + '">' + escapeHtml(x) + '</button>'; }).join('') + '</div>'; 517 } 518 519 function render(raw) { 520 var q = raw.trim(); 521 results = []; activeIndex = -1; 522 els.dym.hidden = true; els.dym.innerHTML = ''; els.intent.innerHTML = ''; 523 if (!q) { 524 els.list.innerHTML = ''; els.status.textContent = ''; 525 els.input.setAttribute('aria-expanded', 'false'); 526 els.all.textContent = 'Browse every page'; els.all.href = SEARCH_PAGE; 527 renderStart(); 528 return; 529 } 530 els.start.hidden = true; 531 if (!index) { 532 els.list.innerHTML = ''; 533 els.status.innerHTML = 'Search is still loading. <a href="' + SEARCH_PAGE + '?q=' + encodeURIComponent(q) + '">Open the full page list</a>.'; 534 return; 535 } 536 var r = resolve(q); 537 var shown = r.shownQuery; 538 if (r.mode === 'auto') { els.dym.hidden = false; els.dym.innerHTML = SVG_REDO + '<span>Showing results for <strong>' + escapeHtml(shown) + '</strong>. <a href="' + SEARCH_PAGE + '?q=' + encodeURIComponent(q) + '&exact=1">Search as typed</a></span>'; } 539 if (r.mode === 'suggest') { els.dym.hidden = false; els.dym.innerHTML = SVG_INFO + '<span>Did you mean <a href="#" data-oq="' + escapeHtml(r.dym) + '">' + escapeHtml(r.dym) + '</a>?</span>'; } 540 var cards = intents(shown); 541 els.intent.innerHTML = renderCards(cards); 542 results = r.results.slice(0, MAX_OVERLAY); 543 els.all.href = SEARCH_PAGE + '?q=' + encodeURIComponent(q); 544 if (!r.results.length) { 545 els.list.innerHTML = ''; 546 els.input.setAttribute('aria-expanded', 'false'); 547 els.status.innerHTML = cards.length ? '' : 'Nothing matched “' + escapeHtml(q) + '”. Try a provider name, your town, a deal type or a speed problem. Or <a href="/compare/">compare live deals at your postcode</a>.'; 548 els.all.textContent = 'Browse every page'; 549 if (!cards.length) track('site_search_zero', q, { surface: 'overlay' }); 550 return; 551 } 552 els.input.setAttribute('aria-expanded', 'true'); 553 els.list.innerHTML = results.map(function (p, i) { 554 return '<li class="bsu-search__result" role="option" id="bsu-r' + i + '" data-i="' + i + '" aria-selected="false">' + 555 '<a href="' + escapeHtml(p.u) + '" tabindex="-1">' + 556 '<span class="bsu-search__t">' + highlight(p.t, shown) + '</span>' + 557 (p.d ? '<span class="bsu-search__d">' + escapeHtml(trim(p.d, 110)) + '</span>' : '') + 558 '<span class="bsu-search__s">' + escapeHtml(p.s) + '</span>' + 559 '</a></li>'; 560 }).join(''); 561 var total = r.results.length; 562 els.status.textContent = total === 1 ? '1 page matches.' : total.toLocaleString('en-GB') + ' pages match. Showing the top ' + results.length + '.'; 563 els.all.textContent = total > results.length ? 'See all ' + total.toLocaleString('en-GB') + ' results' : 'Browse every page'; 564 setActive(cards.length ? -1 : 0, false); 565 } 566 567 function setActive(i, scroll) { 568 var items = els.list.querySelectorAll('li[role="option"]'); 569 for (var n = 0; n < items.length; n++) { 570 items[n].setAttribute('aria-selected', n === i ? 'true' : 'false'); 571 items[n].classList.toggle('is-active', n === i); 572 } 573 activeIndex = i; 574 if (i >= 0 && items[i]) { 575 els.input.setAttribute('aria-activedescendant', 'bsu-r' + i); 576 if (scroll && items[i].scrollIntoView) items[i].scrollIntoView({ block: 'nearest', behavior: reduceMotion ? 'auto' : 'smooth' }); 577 } else { 578 els.input.removeAttribute('aria-activedescendant'); 579 } 580 } 581 582 function openOverlay(seed) { 583 buildOverlay(); 584 lastFocus = document.activeElement; 585 overlay.hidden = false;
586 document.documentElement.classList.add('bsu-search-open'); 587 loadIndex().then(function () { render(els.input.value); }); 588 if (seed) els.input.value = seed; 589 els.input.focus(); els.input.select(); 590 render(els.input.value); 591 } 592 function closeOverlay() { 593 if (!overlay || overlay.hidden) return; 594 overlay.hidden = true; 595 document.documentElement.classList.remove('bsu-search-open'); 596 els.input.value = ''; els.list.innerHTML = ''; els.status.textContent = ''; els.dym.hidden = true; els.intent.innerHTML = ''; 597 results = []; activeIndex = -1; 598 if (lastFocus && lastFocus.focus) lastFocus.focus(); 599 } 600 601 /* ---------------------------------------------------------------- * 602 * Triggers 603 * ---------------------------------------------------------------- */ 604 605 function isSearchPage() { return /\/sitemap\.html$/.test(window.location.pathname); } 606 607 function bindTriggers() { 608 document.addEventListener('click', function (e) { 609 var t = e.target.closest('[data-site-search-open]'); 610 if (t) { e.preventDefault(); openOverlay(); return; } 611 var link = e.target.closest('a[href$="/sitemap.html"], a[href="' + SEARCH_PAGE + '"]'); 612 if (!link) return; 613 if (isSearchPage()) return; 614 if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return; 615 if (!link.closest('header, .site-header, nav, .topbar, .utility-bar')) return; 616 e.preventDefault(); 617 openOverlay(); 618 }); 619 document.addEventListener('keydown', function (e) { 620 var tag = (e.target.tagName || '').toLowerCase(); 621 var typing = tag === 'input' || tag === 'textarea' || tag === 'select' || e.target.isContentEditable; 622 if ((e.key === 'k' || e.key === 'K') && (e.metaKey || e.ctrlKey)) { e.preventDefault(); if (isSearchPage()) focusPageSearch(); else openOverlay(); return; } 623 if (e.key === '/' && !typing && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); if (isSearchPage()) focusPageSearch(); else openOverlay(); } 624 }); 625 var warmed = false; 626 ['pointerover', 'focusin', 'touchstart'].forEach(function (evt) { 627 document.addEventListener(evt, function (e) { 628 if (warmed) return; 629 if (!e.target.closest || !e.target.closest('[data-site-search-open], a[href$="/sitemap.html"]')) return; 630 warmed = true; loadIndex(); 631 }, { passive: true }); 632 }); 633 } 634 635 /* ---------------------------------------------------------------- * 636 * Search page (/sitemap.html) 637 * ---------------------------------------------------------------- */ 638 639 function focusPageSearch() { var input = document.getElementById('q'); if (input) { input.focus(); input.select(); } } 640 641 function ensure(id, cls, after, tag) { 642 var el = document.getElementById(id); 643 if (el) return el; 644 el = document.createElement(tag || 'div'); 645 el.id = id; el.className = cls; 646 if (after && after.parentNode) after.parentNode.insertBefore(el, after.nextSibling); 647 return el; 648 } 649 650 function initSearchPage() { 651 var input = document.getElementById('q'); 652 if (!input) return; 653 injectStyles(); 654 var groups = Array.prototype.slice.call(document.querySelectorAll('[data-search-group]')); 655 var items = Array.prototype.slice.call(document.querySelectorAll('[data-search-item]')); 656 var status = document.getElementById('search-status'); 657 var empty = document.getElementById('search-empty'); 658 var notice = ensure('search-notice', 'sm-notice', status); 659 var intent = ensure('search-intent', 'sm-intent', notice); 660 var recent = ensure('search-recent', 'sm-recent', intent); 661 var filters = ensure('search-filters', 'sm-filters', recent); 662 var total = items.length; 663 var activeSection = 'all'; 664 var azLetter = ''; 665 var lastQuery = null; 666 var itemMeta = items.map(function (li) { 667 var a = li.querySelector('a'); 668 var desc = li.querySelector('.sm-item__desc'); 669 var aText = a ? a.textContent : '', dText = desc ? desc.textContent : ''; 670 return { li: li, a: a, desc: desc, aText: aText, dText: dText, 671 title: (li.getAttribute('data-title') || aText), d: (li.getAttribute('data-desc') || dText), s: (li.getAttribute('data-section') || ''), 672 href: a ? a.getAttribute('href') || '' : '' }; 673 }); 674 itemMeta.forEach(function (m) { m.hay = norm(m.title + ' ' + m.d + ' ' + m.s + ' ' + m.href.replace(/[/.]/g, ' ')); m.tt = norm(m.title); });
675 var groupMeta = groups.map(function (g) { var h = g.querySelector('.sm-group__title'); return { g: g, name: h ? h.firstChild.textContent.trim() : '', count: h ? h.querySelector('.sm-group__count') : null, isDir: /provider directory/i.test(h ? h.textContent : '') }; }); 676 var clearBtn = null; 677 var wrap = input.parentNode; 678 if (wrap && !wrap.querySelector('.sm-clear')) { 679 clearBtn = document.createElement('button'); 680 clearBtn.type = 'button'; clearBtn.className = 'sm-clear'; clearBtn.setAttribute('aria-label', 'Clear search'); clearBtn.hidden = true; 681 clearBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>'; 682 var hint = wrap.querySelector('.sm-search__hint'); 683 wrap.insertBefore(clearBtn, hint || null); 684 clearBtn.addEventListener('click', function () { input.value = ''; apply('', true); input.focus(); }); 685 } 686 687 function matchesTokens(m, tokens) { 688 for (var i = 0; i < tokens.length; i++) { 689 var alts = expand(tokens[i]), ok = false; 690 for (var k = 0; k < alts.length; k++) if (m.hay.indexOf(alts[k]) !== -1) { ok = true; break; } 691 if (!ok) return false; 692 } 693 return true; 694 } 695 function setText(m, q) { 696 if (m.a) m.a.innerHTML = q ? highlight(m.aText, q) : escapeHtml(m.aText); 697 if (m.desc) m.desc.innerHTML = q ? highlight(m.dText, q) : escapeHtml(m.dText); 698 } 699 function renderRecent(q) { 700 var rec = getRecent(); 701 recent.hidden = !!q || !rec.length; 702 recent.innerHTML = rec.length ? '<span>Recent:</span>' + rec.map(function (x) { return '<button type="button" data-q="' + escapeHtml(x) + '">' + escapeHtml(x) + '</button>'; }).join('') : ''; 703 } 704 function renderAz(g) { 705 var existing = g.g.querySelector('.sm-az'); 706 if (existing) existing.parentNode.removeChild(existing); 707 var lis = Array.prototype.slice.call(g.g.querySelectorAll('[data-search-item]')); 708 var letters = {}; 709 lis.forEach(function (li) { var a = li.querySelector('a'); var L = a ? a.textContent.trim().charAt(0).toUpperCase() : ''; if (L) letters[L] = true; }); 710 var keys = Object.keys(letters).sort(); 711 if (!keys.length) return; 712 var az = document.createElement('div'); 713 az.className = 'sm-az'; az.setAttribute('aria-label', 'Jump to letter'); 714 az.innerHTML = keys.map(function (L) { return '<button type="button" data-az="' + L + '" aria-pressed="' + (azLetter === L) + '">' + L + '</button>'; }).join('') + (azLetter ? '<button type="button" data-az="">All</button>' : ''); 715 var ul = g.g.querySelector('ul'); 716 g.g.insertBefore(az, ul || null); 717 } 718 719 function apply(rawQuery, pushState) { 720 var q = rawQuery.trim(); 721 var exact = /(^|&)exact=1(&|$)/.test(window.location.search) && q === (new URLSearchParams(window.location.search).get('q') || ''); 722 var shownQuery = q, mode = 'exact', dym = null; 723 if (q && index && !exact) { 724 var r = resolve(q); 725 shownQuery = r.shownQuery; mode = r.mode; dym = r.dym; 726 if (mode === 'suggest') shownQuery = q; 727 } 728 var tokens = toks(shownQuery); 729 var shown = 0, counts = {}; 730 itemMeta.forEach(function (m) { 731 var hit = !tokens.length || matchesTokens(m, tokens); 732 if (hit && activeSection !== 'all' && m.s !== activeSection.toLowerCase()) hit = false; 733 m.hit = hit; 734 if (hit) { shown++; counts[m.s] = (counts[m.s] || 0) + 1; } 735 }); 736 /* A to Z inside the directory group when browsing */ 737 itemMeta.forEach(function (m) { 738 var li = m.li; 739 var azHide = false;
740 if (!q && azLetter && m.li.closest('[data-search-group]') && /provider directory/i.test(m.s)) { 741 azHide = m.aText.trim().charAt(0).toUpperCase() !== azLetter; 742 } 743 li.hidden = !m.hit || azHide; 744 if (lastQuery !== shownQuery) setText(m, m.hit ? shownQuery : ''); 745 }); 746 lastQuery = shownQuery; 747 groupMeta.forEach(function (g) { 748 var any = g.g.querySelector('[data-search-item]:not([hidden])'); 749 g.g.hidden = !any; 750 if (g.count) { 751 var visible = g.g.querySelectorAll('[data-search-item]:not([hidden])').length; 752 var all = g.g.querySelectorAll('[data-search-item]').length; 753 g.count.textContent = q ? visible + ' of ' + all : String(all); 754 } 755 if (g.isDir && !q) renderAz(g); else { var old = g.g.querySelector('.sm-az'); if (old) old.parentNode.removeChild(old); } 756 }); 757 /* notice */ 758 if (mode === 'auto') notice.innerHTML = SVG_REDO + '<div>Showing results for <a href="#" data-q="' + escapeHtml(shownQuery) + '">' + escapeHtml(shownQuery) + '</a><span class="sm-notice__sub">Search instead for “' + escapeHtml(q) + '” <a href="' + SEARCH_PAGE + '?q=' + encodeURIComponent(q) + '&exact=1">as typed</a>.</span></div>'; 759 else if (mode === 'suggest') notice.innerHTML = SVG_INFO + '<div>Did you mean <a href="#" data-q="' + escapeHtml(dym) + '">' + escapeHtml(dym) + '</a>?</div>'; 760 else notice.innerHTML = ''; 761 notice.hidden = !notice.innerHTML; 762 /* intent cards, then the five best-ranked matches so the answer is at the top */ 763 var topHtml = ''; 764 if (q && index) { 765 var ranked = (mode === 'suggest' ? search(q) : search(shownQuery)).slice(0, 5); 766 if (ranked.length > 1 && shown > 5) { 767 topHtml = '<div class="sm-top"><h4>Best matches</h4><ol>' + ranked.map(function (p) { 768 return '<li><a href="' + escapeHtml(p.u) + '">' + highlight(p.t, shownQuery) + '</a><span class="sm-top__s">' + escapeHtml(p.s) + '</span></li>'; 769 }).join('') + '</ol></div>'; 770 } 771 } 772 intent.innerHTML = (q && index ? renderCards(intents(shownQuery)) : '') + topHtml; 773 /* filters */ 774 var secs = []; 775 groupMeta.forEach(function (g) { var n = g.name.toLowerCase(); if (counts[n]) secs.push({ n: n, label: g.name, c: counts[n] }); }); 776 if (q && secs.length > 1) { 777 var totalHits = 0; for (var s in counts) totalHits += counts[s]; 778 filters.hidden = false; 779 filters.innerHTML = '<button type="button" data-sec="all" aria-pressed="' + (activeSection === 'all') + '">All <b>' + totalHits + '</b></button>' + 780 secs.map(function (x) { return '<button type="button" data-sec="' + escapeHtml(x.label) + '" aria-pressed="' + (activeSection === x.label) + '">' + escapeHtml(x.label) + ' <b>' + x.c + '</b></button>'; }).join(''); 781 } else { filters.hidden = true; filters.innerHTML = ''; } 782 if (status) { 783 status.textContent = !q ? 'Showing all ' + total.toLocaleString('en-GB') + ' pages in ' + groups.length + ' sections.' 784 : shown === 1 ? '1 page matches.' : shown.toLocaleString('en-GB') + ' pages match' + (activeSection !== 'all' ? ' in ' + activeSection : '') + '.'; 785 } 786 if (empty) { 787 var isZero = !!q && shown === 0 && !intent.innerHTML; 788 empty.hidden = !isZero; 789 if (isZero) { var t = empty.querySelector('[data-empty-term]'); if (t) t.textContent = q; track('site_search_zero', q, { surface: 'page' }); } 790 } 791 if (clearBtn) clearBtn.hidden = !q; 792 renderRecent(q); 793 if (pushState && window.history && window.history.replaceState) { 794 var url = window.location.pathname + (q ? '?q=' + encodeURIComponent(q) : ''); 795 window.history.replaceState(null, '', url); 796 } 797 if (pushState && q) { addRecent(q); track('site_search', q, { surface: 'page', results: shown, corrected: mode === 'auto' ? shownQuery : '' }); } 798 } 799 800 var timer = null; 801 input.addEventListener('input', function () { clearTimeout(timer); activeSection = 'all'; azLetter = ''; timer = setTimeout(function () { apply(input.value, true); }, 90); }); 802 input.addEventListener('keydown', function (e) { if (e.key === 'Escape') { input.value = ''; apply('', true); } }); 803 document.addEventListener('click', function (e) { 804 var dq = e.target.closest('[data-q]'); 805 if (dq && !overlay) { e.preventDefault(); input.value = dq.getAttribute('data-q'); activeSection = 'all'; apply(input.value, true); input.focus(); return; } 806 if (dq && overlay && !overlay.contains(dq)) { e.preventDefault(); input.value = dq.getAttribute('data-q'); activeSection = 'all'; apply(input.value, true); input.focus(); return; } 807 var sec = e.target.closest('[data-sec]'); 808 if (sec) { activeSection = sec.getAttribute('data-sec'); apply(input.value, false); return; } 809 var az = e.target.closest('[data-az]'); 810 if (az) { azLetter = az.getAttribute('data-az'); apply(input.value, false); return; } 811 }); 812 813 var params = new URLSearchParams(window.location.search); 814 var seed = params.get('q') || ''; 815 if (seed) input.value = seed; 816 apply(seed, false); 817 if (seed) input.focus(); 818 /* Once the index has loaded, re-run so typo correction and intent cards apply. */ 819 loadIndex().then(function () { apply(input.value, false); }); 820 } 821 822 /* ---------------------------------------------------------------- */ 823
824 function init() { bindTriggers(); initSearchPage(); } 825 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); 826})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.