← back to Commercialrealestate
CPRA deed-source scaffold + draft request (dormant) + deals-flow→/property nav link; v0.18.2
7b78472fe195983a532f3685218de6f5d265facb · 2026-08-07 07:43:06 -0700 · Steve Abrams
Files touched
A docs/cpra-deed-request.mdM package.jsonM public/deals-flow.htmlA scripts/deed-source-cpra.js
Diff
commit 7b78472fe195983a532f3685218de6f5d265facb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Aug 7 07:43:06 2026 -0700
CPRA deed-source scaffold + draft request (dormant) + deals-flow→/property nav link; v0.18.2
---
docs/cpra-deed-request.md | 87 ++++++++++++++++++++++++
package.json | 2 +-
public/deals-flow.html | 2 +-
scripts/deed-source-cpra.js | 158 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 247 insertions(+), 2 deletions(-)
diff --git a/docs/cpra-deed-request.md b/docs/cpra-deed-request.md
new file mode 100644
index 0000000..c601103
--- /dev/null
+++ b/docs/cpra-deed-request.md
@@ -0,0 +1,87 @@
+# CPRA Deed-Parties Request — LA County RR/CC
+
+## NOTES FOR STEVE (not part of the letter — do not send this section)
+
+- **What this unlocks:** the returned grantor-grantee index gives real BUYER/SELLER +
+ recorded sale price + instrument# per property, which lets `scripts/deed-source-cpra.js`
+ replace the current `deedStub()` in `scripts/enrich-property.js`. That is the deed-parties
+ layer for every CRCP property, at **$0**.
+- **Cost / timing reality:** the request itself is **$0**, but expect **1–6 weeks** to get
+ anything back. The RR/CC may charge **staff/programming time** to run a bulk export — the
+ letter explicitly asks them to give an **itemized cost estimate FIRST**, before incurring
+ any charge, so nothing gets billed without your OK.
+- **Format we want:** electronic, machine-readable (CSV or delimited text). If they send a
+ weird format, that's fine — `deed-source-cpra.js` has a `// TODO(cpra-columns):` map to
+ fill in once we see the real column names.
+- **This is outward-facing.** Do **NOT send until you're ready** — it goes to a real county
+ office on your name/contact. Fill in every `[bracketed]` placeholder first.
+- **How to send:** email is fastest (check the RR/CC public-records / PRA contact page for
+ the current address) or certified mail to the address below. Keep a copy + the send date —
+ the 10-day clock starts when they receive it.
+
+---
+
+## LETTER (copy-paste ready — fill in the [brackets])
+
+[Date]
+
+Los Angeles County Registrar-Recorder/County Clerk
+Attn: Records Management / Public Records Act Request
+12400 Imperial Highway
+Norwalk, CA 90650
+
+**Re: California Public Records Act request — grantor-grantee index of recorded transfers (last 24 months)**
+
+To the Records Management / Public Records Act Coordinator:
+
+Pursuant to the California Public Records Act, California Government Code § 7920.000 et seq.
+(the 2023 recodification of the former § 6250 et seq.), I request access to and a copy of the
+following public records maintained by the Registrar-Recorder/County Clerk.
+
+**Records requested:** the grantor-grantee index of recorded real-property transfers (recorded
+deeds and other conveyances) for the most recent available period. For each recorded transfer,
+please include the following fields to the extent they are maintained in your index:
+
+1. Grantor (seller) name(s);
+2. Grantee (buyer) name(s);
+3. Document type (e.g., grant deed, quitclaim deed, trust transfer deed);
+4. Recording date;
+5. Instrument / document number;
+6. Assessor's Identification Number (AIN) / Assessor's Parcel Number (APN), where associated
+ with the recorded instrument;
+7. Documentary transfer tax amount and/or the stated consideration (sale price) declared on the
+ instrument.
+
+**Time period:** the most recent **24 months** available as of the date this request is
+processed. I would also welcome information on whether this index can be provided on an
+**ongoing or periodic (e.g., monthly) basis**, and what a standing/recurring delivery would
+involve.
+
+**Format:** to minimize staff time and cost, I request the records in an **electronic,
+machine-readable format — CSV or delimited text (or an equivalent export)** — rather than paper
+or images, if the index is maintained electronically.
+
+**Fees:** I request a **waiver or reduction of fees**. If a fee waiver is not available, please
+provide an **itemized written cost estimate BEFORE incurring any charges** so I can review and
+authorize (or narrow) the request. Please do not perform any billable work until I have
+confirmed the estimate.
+
+**Segregation of exempt material:** if any portion of the requested records is exempt from
+disclosure, I ask that you **redact or segregate only the exempt fields and produce the
+remainder**, rather than withholding the entire index (Gov. Code § 7922.525). Please also cite
+the specific statutory exemption for anything withheld.
+
+**Response time:** I understand that under Government Code § 7922.535 you will determine within
+**10 days** whether the request seeks disclosable records and will notify me of that
+determination (and of any extension permitted by statute).
+
+Please direct the records and any questions to me at the contact information below. If any part
+of this request is unclear or unduly burdensome, I am glad to confer to narrow or clarify it.
+
+Thank you for your time and assistance.
+
+Sincerely,
+
+[Your Name]
+[Email]
+[Phone]
diff --git a/package.json b/package.json
index 9844cb2..a49379b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "commercialrealestate",
- "version": "0.18.1",
+ "version": "0.18.2",
"private": true,
"description": "LA County CRE investment explorer — multi-firm sourced, Census + assessor enriched, Qwen-analyzed",
"scripts": {
diff --git a/public/deals-flow.html b/public/deals-flow.html
index 045de8f..1d54c8d 100644
--- a/public/deals-flow.html
+++ b/public/deals-flow.html
@@ -97,7 +97,7 @@ select{background:var(--card);border:1px solid var(--line);color:var(--ink);bord
<header>
<h1><b>Closed Deal Flow</b> · LA Commercial</h1>
<span class="badge">100% public record</span>
- <div class="nav"><a href="/just-listed.html">Just Listed</a><a href="/fha-leads.html">Assumable Leads</a><a href="/mls.html">Listings</a><a href="/">Explorer</a></div>
+ <div class="nav"><a href="/just-listed.html">Just Listed</a><a href="/fha-leads.html">Assumable Leads</a><a href="/mls.html">Listings</a><a href="/property">🔍 Property Lookup</a><a href="/">Explorer</a></div>
<span id="auth" style="margin-left:auto;font-size:12px;color:var(--blue);cursor:pointer">Sign in</span>
</header>
<div class="ticker" id="ticker" style="display:none" aria-label="Breaking real estate headlines"><div class="lbl" id="tkLbl">Breaking</div><div class="vp"><div class="track" id="tickTrack"></div></div><button class="tk-ctrl" id="tkCtrl" type="button" aria-pressed="false" aria-label="Stop ticker" title="Stop ticker to read">⏸</button><button class="tk-ctrl" id="tkClose" type="button" aria-label="Hide ticker" title="Hide ticker">✕</button></div>
diff --git a/scripts/deed-source-cpra.js b/scripts/deed-source-cpra.js
new file mode 100644
index 0000000..c545284
--- /dev/null
+++ b/scripts/deed-source-cpra.js
@@ -0,0 +1,158 @@
+#!/usr/bin/env node
+// deed-source-cpra.js — the FREE, $0 CPRA deed-parties adapter for CRCP.
+//
+// WHAT THIS IS: the $0 path that eventually REPLACES enrich-property.js's `deedStub()`
+// with REAL grantor/grantee (seller/buyer) + recorded sale price + instrument# per
+// property. It stays DORMANT until Steve files the California Public Records Act request
+// (see docs/cpra-deed-request.md) and the LA County Registrar-Recorder/County Clerk
+// (RR/CC) returns a grantor-grantee index file (CSV or JSON). Once that file lands,
+// serve.js (or enrich-property.js) can swap `deedStub(dossier)` for:
+// const cpra = require('./deed-source-cpra');
+// const records = cpra.loadCpraIndex('<path-to-returned-index>');
+// const deed = cpra.deedByAin(records, dossier.ain); // {available:true, ...} or {available:false}
+//
+// PUBLIC RECORD ONLY: recorded deeds are public record; this maps a government-returned
+// grantor-grantee index. Nothing proprietary, no paid feed, no scraping — just parsing
+// the file the county sends back under the CPRA request.
+//
+// NODE BUILT-INS ONLY (fs, path) — no npm deps, matches enrich-property.js style.
+'use strict';
+const fs = require('fs');
+const path = require('path');
+
+// The RR/CC-returned column names are UNKNOWN until the CPRA request actually returns a
+// file (the county controls its own export format). Do NOT treat these as verified — they
+// are PROVISIONAL guesses so the adapter has a shape to normalize against. When the real
+// file arrives, open it, read the header row, and correct this map to the true column names.
+//
+// TODO(cpra-columns): replace each right-hand column name below with the EXACT header the
+// RR/CC index file actually uses. Verify against the returned file's header/keys before use.
+const COLS = {
+ ain: 'AIN', // parcel id (may come back as 'APN', 'ParcelNumber', 'PARCEL_ID')
+ grantor: 'Grantor', // seller (may be 'GRANTOR_NAME', 'SellerName')
+ grantee: 'Grantee', // buyer (may be 'GRANTEE_NAME', 'BuyerName')
+ instrument: 'InstrumentNumber', // doc/instrument # (may be 'DocumentNumber', 'DOC_NUM', 'RecordingNumber')
+ recording_date: 'RecordingDate', // (may be 'RECORD_DATE', 'DateRecorded')
+ price: 'SalePrice', // stated consideration / DTT-derived (may be 'Consideration', 'DocTransferTax', null)
+};
+
+const clean = v => (v == null || String(v).trim() === '') ? null : String(v).trim();
+
+// Parse a price-ish cell → number or null. Tolerates "$1,250,000", "1250000.00", blanks.
+function toPrice(v) {
+ const s = clean(v);
+ if (s == null) return null;
+ const n = Number(s.replace(/[^0-9.\-]/g, ''));
+ return Number.isFinite(n) && n !== 0 ? n : null;
+}
+
+// Tolerant CSV line splitter — handles quoted fields, embedded commas, and "" escaped quotes.
+function splitCsvLine(line) {
+ const out = [];
+ let cur = '';
+ let inQ = false;
+ for (let i = 0; i < line.length; i++) {
+ const c = line[i];
+ if (inQ) {
+ if (c === '"') {
+ if (line[i + 1] === '"') { cur += '"'; i++; } // escaped quote
+ else inQ = false;
+ } else cur += c;
+ } else if (c === '"') inQ = true;
+ else if (c === ',') { out.push(cur); cur = ''; }
+ else cur += c;
+ }
+ out.push(cur);
+ return out.map(s => s.trim());
+}
+
+// Minimal CSV → array-of-objects (header row keyed). Tolerates \r\n, blank lines.
+function parseCsv(text) {
+ const lines = text.split(/\r?\n/).filter(l => l.trim() !== '');
+ if (!lines.length) return [];
+ const header = splitCsvLine(lines[0]);
+ const rows = [];
+ for (let i = 1; i < lines.length; i++) {
+ const cells = splitCsvLine(lines[i]);
+ const obj = {};
+ header.forEach((h, j) => { obj[h] = cells[j]; });
+ rows.push(obj);
+ }
+ return rows;
+}
+
+// Map one raw RR/CC row (whatever its columns) → normalized deed record via COLS.
+function normalizeRow(raw) {
+ return {
+ ain: clean(raw[COLS.ain]),
+ grantor: clean(raw[COLS.grantor]),
+ grantee: clean(raw[COLS.grantee]),
+ instrument: clean(raw[COLS.instrument]),
+ recording_date: clean(raw[COLS.recording_date]),
+ price: toPrice(raw[COLS.price]), // nullable — deeds often carry no stated consideration
+ };
+}
+
+// loadCpraIndex(filePath) — read a local .csv or .json RR/CC grantor-grantee index and
+// return normalized records [{ain, grantor, grantee, instrument, recording_date, price}].
+// Detects format by file extension. Throws if the file is missing or unparseable — the
+// caller (a dormant swap-in) decides how to fall back to deedStub() until the file exists.
+function loadCpraIndex(filePath) {
+ const abs = path.resolve(filePath);
+ const text = fs.readFileSync(abs, 'utf8');
+ const ext = path.extname(abs).toLowerCase();
+ let raw;
+ if (ext === '.json') {
+ const parsed = JSON.parse(text);
+ // Accept either a bare array or a wrapper like {records:[...]} / {rows:[...]} / {data:[...]}.
+ raw = Array.isArray(parsed) ? parsed
+ : (parsed.records || parsed.rows || parsed.data || parsed.results || []);
+ } else {
+ // Default everything else (.csv, .txt, delimited) to the tolerant CSV parser.
+ raw = parseCsv(text);
+ }
+ return (raw || []).map(normalizeRow).filter(r => r.ain); // drop rows with no AIN to join on
+}
+
+// deedByAin(records, ain) — return the deed object enrich-property expects for one AIN,
+// in the SAME shape the app renders (deals-flow.html renderDossier reads d.deed.available).
+// If multiple recorded transfers exist for the AIN, return the most recent by recording_date.
+function deedByAin(records, ain) {
+ const key = clean(ain);
+ if (!key) return { available: false };
+ const hits = (records || []).filter(r => r.ain === key);
+ if (!hits.length) return { available: false };
+ hits.sort((a, b) => String(b.recording_date || '').localeCompare(String(a.recording_date || '')));
+ const r = hits[0];
+ return {
+ available: true,
+ grantor: r.grantor,
+ grantee: r.grantee,
+ instrument: r.instrument,
+ recording_date: r.recording_date,
+ price: r.price, // nullable
+ ain: r.ain,
+ source: 'LA County RR/CC grantor-grantee index (CPRA)',
+ };
+}
+
+module.exports = { loadCpraIndex, deedByAin, COLS };
+
+// Tiny CLI for testing once a real RR/CC file exists:
+// node scripts/deed-source-cpra.js --file <path-to-index.csv|.json> --ain <AIN>
+if (require.main === module) {
+ const args = process.argv.slice(2);
+ const fileI = args.indexOf('--file');
+ const ainI = args.indexOf('--ain');
+ if (fileI < 0 || ainI < 0) {
+ console.error('usage: node scripts/deed-source-cpra.js --file <path> --ain <AIN>');
+ process.exit(2);
+ }
+ try {
+ const records = loadCpraIndex(args[fileI + 1]);
+ console.log(JSON.stringify(deedByAin(records, args[ainI + 1]), null, 2));
+ } catch (e) {
+ console.error('error:', e.message);
+ process.exit(1);
+ }
+}
← a6093d9 auto-data-snapshot: 2026-08-07T07:29:27 (2 data files) — dat
·
back to Commercialrealestate
·
auto-data-snapshot: 2026-08-07T08:00:20 (1 data files) — pac d844cd6 →