← back to Ventura Corridor
iter 120: public /find — reader-facing finder by suite/street/biz-name/trade. /api/find?q= tokenizes the query into numeric tokens (street numbers, suite numbers) + alphabetic word tokens (skips stop-words like 'the/in/at/blvd/ave/suite'), scores results 1-4 (4 = address numeric match, 3 = name word match, 2 = address word match, 1 = city or NAICS word match), returns top 30 ordered by score then unfeatured-last. /find.html serves a centered Cormorant 64px italic 'Who's where?' page w/ 28px italic search input + 5 example tap-to-fill chips + result rows showing #suite + biz + headline + addr + ★ feature/building badge; clicking a result with feature → /magazine/:id, otherwise → /buildings/:bldg/issue. ADMIN_PATHS extended
93a2f1a258712f4176d34eeccb278bc2bd038412 · 2026-05-06 18:10:38 -0700 · SteveStudio2
Files touched
A public/find.htmlM src/server/index.ts
Diff
commit 93a2f1a258712f4176d34eeccb278bc2bd038412
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Wed May 6 18:10:38 2026 -0700
iter 120: public /find — reader-facing finder by suite/street/biz-name/trade. /api/find?q= tokenizes the query into numeric tokens (street numbers, suite numbers) + alphabetic word tokens (skips stop-words like 'the/in/at/blvd/ave/suite'), scores results 1-4 (4 = address numeric match, 3 = name word match, 2 = address word match, 1 = city or NAICS word match), returns top 30 ordered by score then unfeatured-last. /find.html serves a centered Cormorant 64px italic 'Who's where?' page w/ 28px italic search input + 5 example tap-to-fill chips + result rows showing #suite + biz + headline + addr + ★ feature/building badge; clicking a result with feature → /magazine/:id, otherwise → /buildings/:bldg/issue. ADMIN_PATHS extended
---
public/find.html | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++
src/server/index.ts | 35 ++++++++++++++++++++++
2 files changed, 121 insertions(+)
diff --git a/public/find.html b/public/find.html
new file mode 100644
index 0000000..4f89ce5
--- /dev/null
+++ b/public/find.html
@@ -0,0 +1,86 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Find · The Corridor</title>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<style>
+@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;1,400;1,500&family=Inter:wght@300;400;500&family=JetBrains+Mono:wght@200;300&display=swap');
+:root{--paper:#faf6ee;--ink:#1a1815;--ink-mute:#6e6356;--metal:#8a6d3b;--metal-glow:#b89968;--accent:#6a3a1a;--rule:#d8cdb8}
+*{box-sizing:border-box}
+html,body{margin:0;background:var(--paper);color:var(--ink);font-family:'Inter',sans-serif;font-weight:300;line-height:1.55}
+header{text-align:center;padding:60px 24px 32px;border-bottom:1px solid var(--rule)}
+header .kicker{font-size:9px;letter-spacing:.4em;text-transform:uppercase;color:var(--metal);font-weight:500}
+header h1{font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:64px;margin:8px 0 0;color:var(--ink);letter-spacing:-0.02em}
+header h1 em{color:var(--metal);font-style:normal}
+header .deck{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:18px;color:var(--ink-mute);margin-top:12px;max-width:540px;margin-left:auto;margin-right:auto}
+.searchbar{padding:32px 24px;text-align:center}
+.searchbar input{font-family:'Cormorant Garamond',serif;font-size:28px;font-style:italic;background:transparent;border:none;border-bottom:1px solid var(--rule);color:var(--ink);padding:14px 12px;min-width:520px;outline:none;text-align:center}
+.searchbar input:focus{border-bottom-color:var(--metal)}
+.searchbar .examples{font-size:11px;letter-spacing:.18em;color:var(--ink-mute);margin-top:12px}
+.searchbar .examples b{color:var(--metal);font-family:'JetBrains Mono',monospace;font-weight:400;background:rgba(184,153,104,0.08);padding:2px 8px;margin:0 3px;cursor:pointer}
+main{max-width:780px;margin:0 auto;padding:24px 24px 80px}
+.row{display:grid;grid-template-columns:auto 1fr auto;gap:16px;align-items:center;padding:16px 0;border-bottom:1px solid var(--rule);text-decoration:none;color:var(--ink)}
+.row:hover{background:rgba(184,153,104,0.04);padding-left:8px;padding-right:8px}
+.row .suite{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--metal);min-width:60px;text-align:right;letter-spacing:.05em}
+.row .biz{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:18px;color:var(--ink)}
+.row .biz small{display:block;font-family:'Inter',sans-serif;font-style:normal;font-size:11px;color:var(--ink-mute);margin-top:2px;letter-spacing:.04em}
+.row .feat{font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:var(--metal-glow);font-weight:500}
+.row .feat.un{color:var(--ink-mute);font-weight:300}
+.empty{text-align:center;padding:80px 24px;color:var(--ink-mute);font-style:italic;font-family:'Cormorant Garamond',serif;font-size:22px}
+.toolbar{position:fixed;top:14px;right:14px;display:flex;gap:6px;z-index:100}
+.toolbar a{font-size:9px;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-mute);background:var(--paper);padding:5px 10px;border:1px solid var(--rule);text-decoration:none}
+.toolbar a:hover{color:var(--metal);border-color:var(--metal)}
+</style>
+</head>
+<body>
+<div class="toolbar">
+ <a href="/issue">Issue</a>
+ <a href="/search.html">Editorial search</a>
+</div>
+<header>
+ <div class="kicker">The Corridor · finder</div>
+ <h1>Who's <em>where</em>?</h1>
+ <div class="deck">Search by suite, street number, business name, or trade. Every BTRC-licensed business on Ventura Boulevard is in here.</div>
+</header>
+<div class="searchbar">
+ <input id="q" type="search" autofocus oninput="onSearch()" placeholder="15303 suite 1600 / lawyer encino / shirin restaurant">
+ <div class="examples">try
+ <b onclick="qy('15442')">15442</b>
+ <b onclick="qy('lawyer encino')">lawyer encino</b>
+ <b onclick="qy('shirin')">shirin</b>
+ <b onclick="qy('chiropractic sherman oaks')">chiropractic sherman oaks</b>
+ <b onclick="qy('15303 suite 1600')">15303 suite 1600</b>
+ </div>
+</div>
+<main id="results"></main>
+<script>
+function escHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
+function qy(s) { document.getElementById('q').value = s; onSearch(); }
+let _t;
+async function onSearch() {
+ clearTimeout(_t);
+ _t = setTimeout(go, 200);
+}
+async function go() {
+ const q = document.getElementById('q').value.trim();
+ if (q.length < 2) {
+ document.getElementById('results').innerHTML = '';
+ return;
+ }
+ const data = await fetch('/api/find?q=' + encodeURIComponent(q)).then(r => r.json());
+ const rows = data.rows || [];
+ document.getElementById('results').innerHTML = rows.length === 0
+ ? `<div class="empty">No matches.</div>`
+ : rows.map(r => `
+ <a class="row" href="${r.feature_id ? `/magazine/${r.feature_id}` : `/buildings/${encodeURIComponent(r.bldg_address || '')}/issue`}">
+ <div class="suite">${escHtml(r.suite ? '#' + r.suite : '')}</div>
+ <div>
+ <div class="biz">${escHtml(r.headline || r.name)}<small>${escHtml(r.name)} · ${escHtml(r.address || '')} · ${escHtml(r.city || '')}</small></div>
+ </div>
+ <div class="feat ${r.feature_id ? '' : 'un'}">${r.feature_id ? '★ feature' : 'building'}</div>
+ </a>`).join('');
+}
+</script>
+</body>
+</html>
diff --git a/src/server/index.ts b/src/server/index.ts
index 80b09c1..f9d5f7d 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -77,6 +77,8 @@ const ADMIN_PATHS = [
/^\/issue(\/[\w-]+)?\/?$/i,
/^\/issue\.pdf\/?$/i,
/^\/search(\.html)?\/?$/i,
+ /^\/find(\.html)?\/?$/i,
+ /^\/api\/find\/?$/i,
/^\/words\/?$/i,
/^\/sitemap\.xml\/?$/i,
/^\/robots\.txt\/?$/i,
@@ -2444,6 +2446,39 @@ app.post('/api/magazine/review-drafts', express.json(), async (req, res) => {
// /sponsor/:id — owner-claim form prefilled with the business + feature. Submits
// via POST /api/sponsor/inquiry which emails Steve via George Gmail.
+// /find — public-facing finder. Readers type "15303 suite 1600" or "the lawyer
+// in encino" and get the closest match in the corridor.
+// Different from /search.html (which searches editorial text); this hits names,
+// addresses, suites, and city directly.
+app.get('/api/find', async (req, res) => {
+ try {
+ const q = String(req.query.q || '').trim();
+ if (q.length < 2) return res.json({ rows: [] });
+ // Tokenize. Numeric tokens hit street_number / suite. Alphabetic tokens hit name + naics.
+ const numTokens = (q.match(/\b\d{2,5}\b/g) || []);
+ const wordTokens = (q.toLowerCase().match(/\b[a-z]{3,}\b/g) || []).filter(w => !['the','and','at','in','of','for','suite','ste','unit','blvd','ave','street','road'].includes(w));
+ const r = await query(
+ `SELECT bc.id AS business_id, bc.name, bc.address, bc.city, bc.suite, bc.bldg_address,
+ mf.id AS feature_id, mf.headline, mf.category_tag,
+ ${numTokens.map((n, i) => `(bc.address ILIKE '%${n}%')::int AS num_${i}`).join(', ') || `0 AS num_0`},
+ ${wordTokens.map((w, i) => `(bc.name ILIKE '%${w.replace(/'/g, "''")}%' OR bc.address ILIKE '%${w.replace(/'/g, "''")}%' OR bc.city ILIKE '%${w.replace(/'/g, "''")}%' OR COALESCE(bc.naics,'') ILIKE '%${w.replace(/'/g, "''")}%')::int AS w_${i}`).join(', ') || `0 AS w_0`},
+ GREATEST(${[
+ ...numTokens.map((n, i) => `CASE WHEN bc.address ILIKE '%${n}%' THEN 4 ELSE 0 END`),
+ ...wordTokens.map((w, i) => `CASE WHEN bc.name ILIKE '%${w.replace(/'/g, "''")}%' THEN 3 WHEN bc.address ILIKE '%${w.replace(/'/g, "''")}%' THEN 2 WHEN bc.city ILIKE '%${w.replace(/'/g, "''")}%' OR COALESCE(bc.naics,'') ILIKE '%${w.replace(/'/g, "''")}%' THEN 1 ELSE 0 END`)
+ ].join(', ') || '0'}) AS score
+ FROM v_building_canonical bc
+ LEFT JOIN magazine_features mf ON mf.business_id = bc.id
+ WHERE ${[...numTokens.map(n => `bc.address ILIKE '%${n}%'`),
+ ...wordTokens.map(w => `(bc.name ILIKE '%${w.replace(/'/g, "''")}%' OR bc.address ILIKE '%${w.replace(/'/g, "''")}%' OR bc.city ILIKE '%${w.replace(/'/g, "''")}%' OR COALESCE(bc.naics,'') ILIKE '%${w.replace(/'/g, "''")}%')`)].join(' OR ') || 'false'}
+ ORDER BY score DESC, mf.id IS NULL ASC, bc.name ASC
+ LIMIT 30`
+ );
+ res.json({ q, rows: r.rows });
+ } catch (e: any) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
// /buildings/:bldg/issue — mini-issue showing every feature for one building.
// Powers a future "this building's tenants" landing page; permalinkable.
app.get('/buildings/:bldg/issue', async (req, res) => {
← e2338c1 iter 118+119: /magazine.html admin meta strip + /buildings/:
·
back to Ventura Corridor
·
iter 121+122: og+JSON-LD on per-vertical /issue/:cat + per-f bd681e7 →