[object Object]

← back to Commercialrealestate

DRE->CRCP-id join: link condo/residential brokers to broker profiles

89fb4917da26166b73b21573869769c0aed92512 · 2026-08-19 10:29:31 -0700 · Steve

- serve.js: GET /api/brokers/by-dre/:license — snapshot-first resolver, digits-only
  match on dre_license then license; returns {id,name} or {id:null}. Works identical
  local + prod (snapshot mode).
- broker.html: accept ?dre=<license> (+ optional &name=) -> resolve to id and render;
  graceful 'not in CRCP index' fallback (name + live CA DRE record link, never a dead end).
- condos.html: unobtrusive 'profile' link next to each listing-agent DRE#.
- residential-brokers.html: name links to profile for CA licenses only (CRCP is CA-focused;
  out-of-state would resolve to 'not in index', so kept high-signal).
Verified E2E on an ephemeral instance (prod :9911 untouched): Cameron Samimi DRE 02035763
-> CRCP id 26, full profile w/ 153 listings; dash-formatted DRE normalizes; bogus DRE ->
graceful fallback. 0 console errors.

Files touched

Diff

commit 89fb4917da26166b73b21573869769c0aed92512
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 19 10:29:31 2026 -0700

    DRE->CRCP-id join: link condo/residential brokers to broker profiles
    
    - serve.js: GET /api/brokers/by-dre/:license — snapshot-first resolver, digits-only
      match on dre_license then license; returns {id,name} or {id:null}. Works identical
      local + prod (snapshot mode).
    - broker.html: accept ?dre=<license> (+ optional &name=) -> resolve to id and render;
      graceful 'not in CRCP index' fallback (name + live CA DRE record link, never a dead end).
    - condos.html: unobtrusive 'profile' link next to each listing-agent DRE#.
    - residential-brokers.html: name links to profile for CA licenses only (CRCP is CA-focused;
      out-of-state would resolve to 'not in index', so kept high-signal).
    Verified E2E on an ephemeral instance (prod :9911 untouched): Cameron Samimi DRE 02035763
    -> CRCP id 26, full profile w/ 153 listings; dash-formatted DRE normalizes; bogus DRE ->
    graceful fallback. 0 console errors.
---
 public/broker.html              | 37 +++++++++++++++++++++++++++++++++----
 public/condos.html              |  4 +++-
 public/residential-brokers.html |  7 ++++++-
 scripts/serve.js                | 20 ++++++++++++++++++++
 4 files changed, 62 insertions(+), 6 deletions(-)

diff --git a/public/broker.html b/public/broker.html
index a4f8229..320f0f4 100644
--- a/public/broker.html
+++ b/public/broker.html
@@ -93,6 +93,8 @@
 const esc = s => String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
 const qs  = new URLSearchParams(location.search);
 const ID  = qs.get('id') || qs.get('broker') || qs.get('dbId');
+const DRE = qs.get('dre') || qs.get('license');   // condo / residential directories link by DRE#
+const NM  = qs.get('name') || '';                 // optional display name for the not-found fallback
 const fmtMoney = v => { const n=Number(String(v).replace(/[^0-9.]/g,'')); if(!n||!isFinite(n)) return null;
   if(n>=1e6) return '$'+(n/1e6).toFixed(n>=1e7?0:1)+'M'; if(n>=1e3) return '$'+Math.round(n/1e3)+'K'; return '$'+n.toLocaleString(); };
 const initials = name => (name||'?').split(/[\s,]+/).filter(Boolean).slice(0,2).map(w=>w[0]).join('').toUpperCase()||'?';
@@ -247,13 +249,40 @@ function render(b){
   renderListings();
 }
 
-(function boot(){
+function loadById(id){
   const root=document.getElementById('root');
-  if(!ID){ root.innerHTML='<div class="empty">No broker id supplied. Open this page as <code>broker.html?id=&lt;brokerId&gt;</code> — e.g. from the <a href="/brokers.html">broker directory</a>.</div>'; return; }
-  fetch('/api/brokers/contact/'+encodeURIComponent(ID))
+  return fetch('/api/brokers/contact/'+encodeURIComponent(id))
     .then(r=>{ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); })
     .then(b=>{ if(!b||b.error||b.id==null){ throw new Error(b&&b.error||'broker not found'); } render(b); })
-    .catch(e=>{ root.innerHTML='<div class="empty">Could not load broker <b>'+esc(ID)+'</b>: '+esc(e.message)+'.<br><a href="/brokers.html">← back to directory</a></div>'; });
+    .catch(e=>{ root.innerHTML='<div class="empty">Could not load broker <b>'+esc(id)+'</b>: '+esc(e.message)+'.<br><a href="/brokers.html">← back to directory</a></div>'; });
+}
+// Graceful "not in the CRCP commercial index" state — shown when a ?dre= link (from the condo /
+// residential directories) points at a license we haven't harvested as a Crexi commercial broker.
+// Never a dead end: shows the name we were handed + a live CA DRE record link.
+function renderNotInIndex(dre,name){
+  const root=document.getElementById('root'); const digits=String(dre||'').replace(/\D/g,'');
+  root.innerHTML='<div class="card" style="margin-top:14px">'
+    +'<h2>Not in the CRCP commercial index</h2>'
+    +'<div class="bio">'+(name?'<b>'+esc(name)+'</b> ':'This license ')+'isn’t in our Crexi-sourced commercial broker index yet'
+    +(digits?', so there’s no CRCP deal profile to show.':'.')+'</div>'
+    +(digits?'<div class="kv" style="margin-top:12px"><span class="ic">🪪</span><span>DRE #'+esc(digits)
+      +' · <a href="https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id='+encodeURIComponent(digits)
+      +'" target="_blank" rel="noopener noreferrer">view the CA DRE license record →</a></span></div>':'')
+    +'<div class="indexed" style="margin-top:12px">CRCP indexes ~2,500 commercial brokers from Crexi. Residential / condo listing agents are matched by DRE# only when they also appear in that set.</div>'
+    +'<div style="margin-top:12px"><a href="/brokers.html">← back to broker directory</a></div></div>';
+  document.title=(name||'Broker')+' — not in CRCP index';
+}
+(function boot(){
+  const root=document.getElementById('root');
+  if(ID){ loadById(ID); return; }
+  if(DRE){
+    fetch('/api/brokers/by-dre/'+encodeURIComponent(String(DRE).replace(/\D/g,'')))
+      .then(r=>r.json())
+      .then(m=>{ if(m && m.id!=null) loadById(m.id); else renderNotInIndex(DRE,NM); })
+      .catch(()=>renderNotInIndex(DRE,NM));
+    return;
+  }
+  root.innerHTML='<div class="empty">No broker supplied. Open this page as <code>broker.html?id=&lt;brokerId&gt;</code> or <code>broker.html?dre=&lt;license#&gt;</code> — e.g. from the <a href="/brokers.html">broker directory</a>.</div>';
 })();
 </script>
 </body>
diff --git a/public/condos.html b/public/condos.html
index 5e28302..f781f2f 100644
--- a/public/condos.html
+++ b/public/condos.html
@@ -322,7 +322,9 @@ function fcell(c,f){ const v=fval(c,f);
     const extra=(Array.isArray(c.agents)&&c.agents.length>1)?` <span style="color:var(--mut);font-size:10px" title="${c.agents.length-1} co-agent license(s) — see card/detail">+${c.agents.length-1}</span>`:'';
     if(!/^\d{6,8}$/.test(String(primary))) return esc(String(primary))+extra;   // defensive: only link a real DRE#
     const u=c.dre_url||('https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id='+primary);
-    return `<a href="${esc(u)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record">${esc(primary)} ↗</a>${extra}`; }
+    const pn=c.broker_name||(c.agents&&c.agents[0]&&c.agents[0].name)||'';
+    const prof=` <a href="/broker.html?dre=${encodeURIComponent(String(primary).replace(/\D/g,''))}${pn?'&name='+encodeURIComponent(pn):''}" title="CRCP broker profile (if in our commercial index)" style="text-decoration:none">👤</a>`;
+    return `<a href="${esc(u)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record">${esc(primary)} ↗</a>${prof}${extra}`; }
   // ── contact-block cells (TK-10243) — resolve nested/flat, phone→tel, email→mailto, sortable text ──
   if(f.k==='agent_phone'){ const p=v||(c.agents&&c.agents[0]&&c.agents[0].phone); return p?`<a href="${telHref(p)}">${esc(p)}</a>`:'—'; }
   if(f.k==='office_phone'){ return v?`<a href="${telHref(v)}">${esc(v)}</a>`:'—'; }
diff --git a/public/residential-brokers.html b/public/residential-brokers.html
index 27c0ce4..ce8dc30 100644
--- a/public/residential-brokers.html
+++ b/public/residential-brokers.html
@@ -96,7 +96,12 @@
 // and the all-field sort menu. `get` = sortable/plain value; `render` = optional HTML.
 // `lock:true` = contact columns that can never be hidden (Steve hard rule).
 const FIELDS = [
-  {k:'name',    label:'Name',    get:r=>r.name},
+  {k:'name',    label:'Name',    get:r=>r.name,
+     // Link to the CRCP broker profile ONLY for CA licenses (CRCP's commercial index is CA-focused,
+     // so an out-of-state license would almost always resolve to "not in index"). broker.html?dre=
+     // degrades gracefully when the DRE isn't in the commercial set.
+     render:r=>{const nm=r.name||'';const lic=r.license_no;const ca=(r.license_state==='CA'||r.state_code==='CA');
+       return (lic&&ca)?`<a href="/broker.html?dre=${encodeURIComponent(String(lic).replace(/[^0-9]/g,''))}&name=${encodeURIComponent(nm)}" onclick="event.stopPropagation()" title="CRCP broker profile (if in our commercial index)">${esc(nm)}</a>`:esc(nm);}},
   {k:'firm',    label:'Firm',    get:r=>r.firm_name},
   {k:'city',    label:'City',    get:r=>[r.city,r.state_code].filter(Boolean).join(', ')},
   {k:'license', label:'License', get:r=>r.license_no ? (r.license_no+(r.license_state?' · '+r.license_state:'')) : ''},
diff --git a/scripts/serve.js b/scripts/serve.js
index 6daacc3..6334ff0 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -336,6 +336,26 @@ app.get('/api/brokers/contact/:id', async (req, res) => {
   } catch (e) { const b = snapContact(req.params.id); return b ? res.json(b) : res.status(502).json({ error: String(e.message).split('\n')[0] }); }
 });
 
+// Resolve a CRCP broker id from a DRE / license number. Powers /broker.html?dre=<n> links from the
+// condo + residential directories, whose row ids are a DIFFERENT id space (condo id / usre-DRE id)
+// than the CRCP broker table. Snapshot-first (the snapshot carries dre_license + license for all
+// brokers and is what prod serves): digits-only match on dre_license first, then the raw license.
+// Returns {id,name} on a hit or {id:null} on a miss (broker.html renders a graceful fallback).
+const snapByDre = (lic) => {
+  const digits = String(lic || '').replace(/\D/g, ''); if (digits.length < 5) return null;
+  const s = readBrokerSnap(); if (!s) return null;
+  const norm = v => String(v == null ? '' : v).replace(/\D/g, '');
+  const b = (s.brokers || []).find(x => norm(x.dre_license) === digits)
+         || (s.brokers || []).find(x => norm(x.license) === digits);
+  return b ? { id: b.id, name: b.name } : null;
+};
+app.get('/api/brokers/by-dre/:license', (req, res) => {
+  const digits = String(req.params.license || '').replace(/\D/g, '');
+  if (digits.length < 5) return res.status(400).json({ error: 'bad license', id: null });
+  const b = snapByDre(digits);
+  res.json(b || { id: null });
+});
+
 // OUR-OWN agent profile by NAME (Steve 2026-08-19: "click any agent -> a page about their
 // listings, built from OUR data, NEVER crexi"). Resolves the broker by name from our graph and
 // returns their contact + the listings WE have for them (broker_listing -> listing). Deliberately

← 435a304 chore: lint (node --check clean, no linter configured) + ref  ·  back to Commercialrealestate  ·  Fold 8,796 new active broker-book listings into catalog (nat a04fb2b →