← back to Ventura Corridor
wire USPTO trademark search into Ventura Corridor (/trademarks.html + /api/trademarks via node:sqlite)
a7d939eb8c712674d6d60d9b02da6c285355d813 · 2026-08-07 08:15:19 -0700 · Steve Abrams
Files touched
M public/index.htmlA public/trademarks.htmlM src/server/index.ts
Diff
commit a7d939eb8c712674d6d60d9b02da6c285355d813
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Aug 7 08:15:19 2026 -0700
wire USPTO trademark search into Ventura Corridor (/trademarks.html + /api/trademarks via node:sqlite)
---
public/index.html | 1 +
public/trademarks.html | 100 +++++++++++++++++++++++++++++++++++++++++++++++++
src/server/index.ts | 48 ++++++++++++++++++++++++
3 files changed, 149 insertions(+)
diff --git a/public/index.html b/public/index.html
index 21de691..1d61fee 100644
--- a/public/index.html
+++ b/public/index.html
@@ -148,6 +148,7 @@
<a class="b-3d" href="/tongues.html" style="margin-top:6px">Tongues — H1s in geographic position ↗</a>
<a class="b-3d" href="/chains.html" style="margin-top:6px">Chains — the parking-economy story ↗</a>
<a class="b-3d" href="/atlas.html" style="margin-top:6px">Atlas — corridor at a glance ↗</a>
+ <a class="b-3d" href="/trademarks.html" style="margin-top:6px">Trademarks — the IP behind the marks ↗</a>
<a class="b-3d" href="/eulogy.html" style="margin-top:6px">Eulogy — websites that no longer answer ↗</a>
<a class="b-3d" href="/marketplace.html" style="margin-top:6px;border-color:var(--metal-glow);color:var(--metal-glow)"><b>Marketplace</b> · local board ↗</a>
<a class="b-3d" href="/today.html" style="margin-top:6px;border-color:var(--metal-glow);color:var(--metal-glow)"><b>Today</b> · daily action plan ↗</a>
diff --git a/public/trademarks.html b/public/trademarks.html
new file mode 100644
index 0000000..745011e
--- /dev/null
+++ b/public/trademarks.html
@@ -0,0 +1,100 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Trademarks · The Corridor</title>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<link rel="icon" type="image/svg+xml" href="/favicon.svg">
+<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;400&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:560px;margin-left:auto;margin-right:auto}
+.searchbar{padding:32px 24px 8px;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;max-width:92vw;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:14px}
+.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}
+.count{text-align:center;font-size:10px;letter-spacing:.28em;text-transform:uppercase;color:var(--ink-mute);margin:6px 0 0}
+main{max-width:860px;margin:0 auto;padding:20px 24px 90px}
+.tm{display:grid;grid-template-columns:1fr auto;gap:6px 18px;padding:18px 0;border-bottom:1px solid var(--rule)}
+.tm:hover{background:rgba(184,153,104,0.04);padding-left:8px;padding-right:8px}
+.tm .mark{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:24px;color:var(--ink);line-height:1.15}
+.tm .owner{font-size:12px;color:var(--ink-mute);letter-spacing:.03em;margin-top:2px}
+.tm .owner b{color:var(--metal);font-style:normal;font-weight:500}
+.tm .gs{grid-column:1/2;font-size:12px;color:var(--ink-mute);margin-top:6px;max-width:620px}
+.tm .side{grid-column:2/3;grid-row:1/3;text-align:right;min-width:150px}
+.tm .status{display:inline-block;font-size:9px;letter-spacing:.2em;text-transform:uppercase;color:var(--metal-glow);border:1px solid var(--rule);padding:3px 8px;font-weight:500}
+.tm .cls{display:block;font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--metal);margin-top:8px;letter-spacing:.05em}
+.tm .filed{display:block;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--ink-mute);margin-top:4px;letter-spacing:.05em}
+.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="/find.html">Business finder</a>
+ <a href="/">← Corridor</a>
+</div>
+<header>
+ <div class="kicker">The Corridor · trademark registry</div>
+ <h1>Who owns the <em>mark</em>?</h1>
+ <div class="deck">Search the USPTO trademark full-text record by word mark or by owner. Surface the IP footprint behind any business or brand on the boulevard.</div>
+</header>
+<div class="searchbar">
+ <input id="q" type="search" autofocus oninput="onSearch()" placeholder="a word mark, or a company / owner name">
+ <div class="examples">try
+ <b onclick="qy('APPLE')">APPLE</b>
+ <b onclick="qy('cider')">cider</b>
+ <b onclick="qy('ventures llc')">ventures llc</b>
+ <b onclick="qy('coffee')">coffee</b>
+ </div>
+ <div class="count" id="count"></div>
+</div>
+<main id="results"></main>
+<script>
+function escHtml(s){return String(s ?? '').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
+function titleCase(s){return String(s ?? '').toLowerCase().replace(/\b\w/g,c=>c.toUpperCase());}
+function qy(s){document.getElementById('q').value=s;onSearch();}
+let _t;
+function onSearch(){clearTimeout(_t);_t=setTimeout(go,200);}
+async function go(){
+ const q=document.getElementById('q').value.trim();
+ const countEl=document.getElementById('count');
+ if(q.length<2){document.getElementById('results').innerHTML='';countEl.textContent='';return;}
+ let data;
+ try{ data=await fetch('/api/trademarks?q='+encodeURIComponent(q)).then(r=>r.json()); }
+ catch(e){ document.getElementById('results').innerHTML=`<div class="empty">Lookup failed.</div>`; return; }
+ const rows=data.rows||[];
+ countEl.textContent = rows.length ? `${data.count}${rows.length>=100?'+':''} match${rows.length===1?'':'es'}` : '';
+ document.getElementById('results').innerHTML = rows.length===0
+ ? `<div class="empty">No trademarks match “${escHtml(q)}”.</div>`
+ : rows.map(r=>{
+ const owner = titleCase(r.owner_name||'');
+ const loc = [r.owner_city&&titleCase(r.owner_city), r.owner_state].filter(Boolean).join(', ');
+ const filed = r.filing_date ? String(r.filing_date).replace(/^(\d{4})(\d{2})(\d{2})$/,'$1-$2-$3') : '';
+ const reg = r.registration_number ? ` · Reg ${escHtml(r.registration_number)}` : '';
+ return `<div class="tm">
+ <div>
+ <div class="mark">${escHtml(r.mark||'—')}</div>
+ <div class="owner"><b>${escHtml(owner||'—')}</b>${loc?' · '+escHtml(loc):''}${reg}</div>
+ </div>
+ <div class="side">
+ <span class="status">${escHtml(r.status_label||'—')}</span>
+ ${r.primary_class?`<span class="cls">Int'l class ${escHtml(r.primary_class)}</span>`:''}
+ ${filed?`<span class="filed">filed ${escHtml(filed)}</span>`:''}
+ </div>
+ ${r.goods_services?`<div class="gs">${escHtml(String(r.goods_services).slice(0,220))}${r.goods_services.length>220?'…':''}</div>`:''}
+ </div>`;
+ }).join('');
+}
+</script>
+</body>
+</html>
diff --git a/src/server/index.ts b/src/server/index.ts
index 884bb0e..6b208d1 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -3758,6 +3758,54 @@ app.get('/api/find', async (req, res) => {
}
});
+// ─── Trademark lookup (USPTO TRTYRAP) ───────────────────────────────────────
+// Read-only search over the USPTO trademark DB owned by the uspto-trtyrap project.
+// Ventura is Postgres-backed, but this one dataset is a standalone SQLite file, so
+// we open it with the built-in node:sqlite driver (no new dependency) and keep a
+// single lazily-opened read-only handle. Matches a query against the word mark OR
+// the owner name — the corridor angle is "what IP does this business/owner hold?".
+import { DatabaseSync } from 'node:sqlite';
+const TRADEMARK_DB = process.env.TRADEMARK_DB
+ || '/Users/macstudio3/Projects/uspto-trtyrap/db/trtyrap.sqlite';
+let _tmDb: InstanceType<typeof DatabaseSync> | null | undefined;
+function trademarkDb() {
+ if (_tmDb !== undefined) return _tmDb; // opened (or already known-missing)
+ try {
+ _tmDb = new DatabaseSync(TRADEMARK_DB, { readOnly: true });
+ } catch {
+ _tmDb = null; // DB absent → endpoint returns [] gracefully
+ }
+ return _tmDb;
+}
+const TM_STATUS: Record<string, string> = {
+ '681': 'Registered / renewed', '700': 'Registered', '710': 'Cancelled',
+ '606': 'Abandoned', '641': 'Opposition pending', '638': 'Suspended',
+ '630': 'Live / pending', '930': 'Live / pending',
+};
+app.get('/api/trademarks', (req, res) => {
+ try {
+ const q = String(req.query.q || '').trim();
+ if (q.length < 2) return res.json({ q, rows: [] });
+ const db = trademarkDb();
+ if (!db) return res.json({ q, rows: [], note: 'trademark DB unavailable' });
+ const like = `%${q.toUpperCase()}%`;
+ // Parameterized (no interpolation) — mark_norm/owner_name are the indexed columns.
+ const rows = db.prepare(
+ `SELECT mark, owner_name, owner_city, owner_state, owner_country,
+ status_code, primary_class, intl_classes, filing_date,
+ registration_number, goods_services
+ FROM trademark
+ WHERE mark_norm LIKE ? OR owner_name LIKE ?
+ ORDER BY (mark_norm = ?) DESC, filing_date DESC
+ LIMIT 100`
+ ).all(like, like, q.toUpperCase()) as any[];
+ for (const r of rows) r.status_label = TM_STATUS[r.status_code as string] || (r.status_code ? `Code ${r.status_code}` : '—');
+ res.json({ q, count: rows.length, rows });
+ } catch (e: any) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
// /api/magazine/:id/social-pack.json — JSON shape of the ad-pack page.
// Programmatic consumers (IG agent, bots) get short/medium/long brief variants
// + tags + share-card url + audio url + permalink in one call.
← 28b0f74 auto-save: 2026-07-13T10:54:28 (1 files) — scripts/push-news
·
back to Ventura Corridor
·
move Ventura 9780->9785 (ga-allsites owns 9780) + PUBLIC_BAS 8be712a →