[object Object]

← back to Rentv Licensed Targets

A2: CA DOI title-insurer scraper (CDI APEX) — resilient (insurers checkpointed before crash-prone UTC universe, UTCs isolated) — 18 real CA-licensed title insurers → CSV [TK-10310]

1b043a9743534e7a76b0cc66c4c49c853c988921 · 2026-08-06 14:43:21 -0700 · Steve

Files touched

Diff

commit 1b043a9743534e7a76b0cc66c4c49c853c988921
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 6 14:43:21 2026 -0700

    A2: CA DOI title-insurer scraper (CDI APEX) — resilient (insurers checkpointed before crash-prone UTC universe, UTCs isolated) — 18 real CA-licensed title insurers → CSV [TK-10310]
---
 data/ca_doi_title.csv     |  19 +++
 scrapers/ca_doi_title.mjs | 328 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 347 insertions(+)

diff --git a/data/ca_doi_title.csv b/data/ca_doi_title.csv
new file mode 100644
index 0000000..fb8f6df
--- /dev/null
+++ b/data/ca_doi_title.csv
@@ -0,0 +1,19 @@
+source,role,entity_name,contact_name,license_no,license_type,license_status,address,city,county,state,zip,phone,website,market,commercial_flag,within_300mi,source_url
+"ca_doi_title","Title","AMERICAN GUARANTY TITLE INSURANCE COMPANY","","4962-7","Title Insurer","Unlimited-Normal","4040 N Tulsa Ave","Oklahoma City","","CA","73112","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:105099,51411&cs=1oh8dl3fQaYy0ramsehvppYNBi7M"
+"ca_doi_title","Title","AMTRUST TITLE INSURANCE COMPANY","","6313-1","Title Insurer","Unlimited-Normal","59 Maiden Lane, 43rd Floor","New York","","CA","10038","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:63327,51578&cs=1CVKB9-M2xakXen-TV9Y19Q218Sw"
+"ca_doi_title","Title","ATLAS NATIONAL TITLE INSURANCE COMPANY","","6274-5","Title Insurer","Unlimited-Normal","16755 Von Karman Avenue, Ste. 200","Irvine","","CA","92606","","","Orange County","true","true","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:116300,16827&cs=1I3khTJpwHqNHqupIjRXzHj8cGSo"
+"ca_doi_title","Title","CHICAGO TITLE INSURANCE COMPANY","","1709-5","Title Insurer","Unlimited-Normal","601 Riverside Ave","Jacksonville","","CA","32204","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:3624,50229&cs=1VMtEqYIc88pvW9PnRauP6uueCqs"
+"ca_doi_title","Title","COMMONWEALTH LAND TITLE INSURANCE COMPANY","","1979-4","Title Insurer","Unlimited-Normal","601 Riverside Avenue","Jacksonville","","CA","32204","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:3890,50083&cs=1bc9w4Pg26nrrNRPUBtbqNDpkieE"
+"ca_doi_title","Title","ESSENT TITLE INSURANCE, INC.","","6257-0","Title Insurer","Unlimited-Normal","100 Mastonford Road","Radnor","","CA","19087","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:116260,12522&cs=1tLSCzqQfPdSxZXPefyn0zJ9O7Xs"
+"ca_doi_title","Title","FIDELITY NATIONAL TITLE INSURANCE COMPANY","","2834-0","Title Insurer","Unlimited-Normal","601 Riverside Avenue","Jacksonville","","CA","32204","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:6571,51586&cs=1W2sxIcm6RElPVCgBnF-MUQqsCfM"
+"ca_doi_title","Title","FIRST AMERICAN TITLE INSURANCE COMPANY","","1951-3","Title Insurer","Unlimited-Normal","4909 S 135th St Ste 207","Omaha","","CA","68137","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:3863,50814&cs=1xqFhY6URLInt77fl77tzGTO-_s0"
+"ca_doi_title","Title","NATIONAL TITLE INSURANCE OF NEW YORK, INC.","","1849-9","Title Insurer","Unlimited-Normal","485 Lexington Ave Fl 18","New York","","CA","10017","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:3764,51020&cs=1BTfIu3XLMW1g0Gz3P9gNqBWstl4"
+"ca_doi_title","Title","OLD REPUBLIC NATIONAL TITLE INSURANCE COMPANY","","1644-4","Title Insurer","Unlimited-Normal","3000 Bayport Dr Ste 1000","Tampa","","CA","33607","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:3559,50520&cs=1QWVDIR56KfTLvRJppZ3OyWyG78k"
+"ca_doi_title","Title","PREMIER LAND TITLE INSURANCE COMPANY","","4296-0","Title Insurer","Unlimited-Normal","9111 Cypress Waters Boulevard, Suite 200","Coppell","","CA","75019","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:71290,50026&cs=1IBezqP75trK9v80H9J26cO4_CEo"
+"ca_doi_title","Title","RADIAN TITLE INSURANCE INC.","","5093-0","Title Insurer","Unlimited-Normal","6100 Oak Tree Road, #200","Independence","","CA","44131","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:107040,51632&cs=1luu6FmkgqGgFTns9-myU3lF_IU4"
+"ca_doi_title","Title","REAL ADVANTAGE TITLE INSURANCE COMPANY","","5906-3","Title Insurer","Unlimited-Normal","1551 N Tustin Ave Ste 430","Santa Ana","","CA","92705","","","Orange County","true","true","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:109616,50440&cs=1UcPY97CuGsgmAn1jxdIHtugwVd8"
+"ca_doi_title","Title","ROCKET TITLE INSURANCE COMPANY","","6392-5","Title Insurer","Unlimited-Normal","5910 N Central Expy Ste 1445","Dallas","","CA","75206","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:111032,11974&cs=1X8uZ_pBAmhEHhT40FmbV9WRFwt0"
+"ca_doi_title","Title","STEWART TITLE GUARANTY COMPANY","","1576-8","Title Insurer","Unlimited-Normal","1360 Post Oak Boulevard, Suite 100","Houston","","CA","77056","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:3492,50121&cs=1zO4GsWXopmteCcBaPcP3EhkHoPA"
+"ca_doi_title","Title","TITLE RESOURCES GUARANTY COMPANY","","5565-7","Title Insurer","Unlimited-Normal","8111 Lyndon B Johnson Fwy Ste 1200","Dallas","","CA","75251","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:63240,50016&cs=10ywxa9Mo76R-v8TH8ScHC7HGK8k"
+"ca_doi_title","Title","WESTCOR LAND TITLE INSURANCE COMPANY","","4580-7","Title Insurer","Unlimited-Normal","1201 Main Street, 1918","Columbia","","CA","29201","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:100169,50050&cs=1KPc3iSj_u4834Vr9_eL6SXeW-BQ"
+"ca_doi_title","Title","WFG NATIONAL TITLE INSURANCE COMPANY","","5872-7","Title Insurer","Unlimited-Normal","400 International Pkwy Ste 160","Lake Mary","","CA","32746","","","CA (statewide)","true","false","https://interactive.web.insurance.ca.gov/apex_extprd/f?p=144:6:7387424598889:::RP,6:P6_COMPANY_ID,P6_NAIC:109678,51152&cs=1xR378ItV7zSOVwLjO8PlKmVmBfc"
diff --git a/scrapers/ca_doi_title.mjs b/scrapers/ca_doi_title.mjs
new file mode 100644
index 0000000..c3ffcf3
--- /dev/null
+++ b/scrapers/ca_doi_title.mjs
@@ -0,0 +1,328 @@
+#!/usr/bin/env node
+// ============================================================================
+// CA Dept of Insurance (CDI) — Title-insurance registry scraper  →  CSV
+// ----------------------------------------------------------------------------
+// Authorized public-registry scrape for the RENTV CRE directory (TK-10310 /
+// TK-10076 A2). Drives the CDI Oracle-APEX public license-lookup app with a
+// real headless-Chromium browser (Playwright) because the app is stateful and
+// checksum-protected — plain fetch can't produce valid session/cs tokens.
+//
+// TWO universes are harvested (both live under app 144):
+//   1. Lines of Insurance Search (f?p=144:10) → Line of Insurance = "Title"
+//      → the licensed TITLE INSURERS (~18).
+//   2. Other Insurance Entities (f?p=144:20) → "Underwritten Title Companies"
+//      → the licensed UTCs (~99, paginated 50/page).
+//
+// For every entity we open its checksum-signed Company Profile detail page
+// (in-session — a hand-built P6 URL is rejected with "Session state protection
+// violation") and read the real mailing address, CA license #, status, and
+// domicile state exposed there.
+//
+// HONESTY (non-negotiable): every field is copied verbatim from the CDI page.
+// The site exposes NO phone and NO website for these entities → those columns
+// are ALWAYS EMPTY (blank by source, not by bug). County is derived from the
+// address ZIP only when the ZIP is a known SoCal band; otherwise left blank —
+// never invented. No fabricated license numbers, no guessed contacts, no
+// hallucinated companies. Only entities the CDI lookup actually returns.
+//
+// Polite: headless, ONE browser session, ~1s pause between detail hits.
+// Output CSV columns match dw_unified.rentv_licensed_targets.
+// $0 (local Playwright, no paid API). Writes CSV only — NO database writes.
+// ============================================================================
+import { chromium } from 'playwright';
+import { writeFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, resolve } from 'node:path';
+
+const __dir = dirname(fileURLToPath(import.meta.url));
+const OUT = resolve(__dir, '..', 'data', 'ca_doi_title.csv');
+const BASE = 'https://interactive.web.insurance.ca.gov/apex_extprd';
+const P10 = BASE + '/f?p=144:10'; // Lines of Insurance Search
+const P20 = BASE + '/f?p=144:20'; // Other Insurance Entities
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+const POLITE_MS = 1000; // ~1 req/s between detail-page hits
+
+// --- market / geo derivation (per TK-10310 spec) ---------------------------
+function market(zip) {
+  const z = parseInt(zip, 10);
+  if (!z) return 'CA (statewide)';
+  if (z >= 90001 && z <= 91899) return 'Greater LA';
+  if (z >= 91900 && z <= 92199) return 'San Diego';
+  if (z >= 92200 && z <= 92599) return 'Inland Empire';
+  if (z >= 92600 && z <= 92899) return 'Orange County';
+  if (z >= 93000 && z <= 93099) return 'Ventura';
+  if (z >= 90000 && z <= 96199) return 'SoCal (other)'; // other CA zips
+  return 'CA (statewide)'; // HQ out of CA
+}
+function isSoCal(zip) {
+  const z = parseInt(zip, 10);
+  return !!z && z >= 90001 && z <= 93099;
+}
+
+// --- CSV helpers -----------------------------------------------------------
+const COLS = ['source','role','entity_name','contact_name','license_no','license_type',
+  'license_status','address','city','county','state','zip','phone','website','market',
+  'commercial_flag','within_300mi','source_url'];
+const cell = v => '"' + String(v == null ? '' : v).replace(/"/g, '""') + '"';
+
+// --- APEX interaction helpers ----------------------------------------------
+// APEX pops a jQuery-UI "Confirm this search? / OK" dialog on submit.
+async function clickConfirmOK(page) {
+  await sleep(1200);
+  try {
+    await page.getByRole('button', { name: /^OK$/i }).click({ timeout: 5000 });
+  } catch { /* no dialog present — fine */ }
+  await page.waitForLoadState('networkidle', { timeout: 60000 }).catch(() => {});
+  await sleep(1500);
+}
+
+// Set a native <select> AND fire the change event APEX listens for.
+async function setSelect(page, id, label) {
+  await page.selectOption('#' + id, { label });
+  await page.evaluate(sid => {
+    document.getElementById(sid).dispatchEvent(new Event('change', { bubbles: true }));
+  }, id);
+}
+
+// Read the results Interactive-Report table on the current page.
+async function readResultTable(page) {
+  return page.evaluate(() => {
+    const t = Array.from(document.querySelectorAll('table'))
+      .find(t => t.rows.length > 1 && /Name/i.test(t.rows[0].innerText) &&
+                 /Company ID|EID/i.test(t.rows[0].innerText));
+    if (!t) return [];
+    const out = [];
+    for (let i = 1; i < t.rows.length; i++) {
+      const cells = Array.from(t.rows[i].cells);
+      const link = cells[0].querySelector('a');
+      out.push({
+        name: cells[0].innerText.trim(),
+        eid: (cells[1] || {}).innerText ? cells[1].innerText.trim() : '',
+        naic: (cells[2] || {}).innerText ? cells[2].innerText.trim() : '',
+        href: link ? link.getAttribute('href') : ''
+      });
+    }
+    return out;
+  });
+}
+
+// Is there an enabled "Next" pagination control?
+async function hasNext(page) {
+  return page.evaluate(() => {
+    const b = Array.from(document.querySelectorAll('a,button'))
+      .find(a => /^\s*Next\s*$/i.test((a.innerText || '').trim()));
+    if (!b) return false;
+    return !b.classList.contains('is-disabled') && !b.disabled &&
+           b.getAttribute('aria-disabled') !== 'true';
+  });
+}
+
+// Parse the Company Profile detail page (currently loaded) into fields.
+async function readDetail(page) {
+  return page.evaluate(() => {
+    const txt = document.body.innerText;
+    const lines = txt.split('\n').map(s => s.trim());
+    // labelled reference fields render as "<Label>\n<value>"
+    const after = (label) => {
+      for (let i = 0; i < lines.length - 1; i++) {
+        if (lines[i] === label) {
+          const v = lines[i + 1];
+          return (v && v !== '-') ? v : '';
+        }
+      }
+      return '';
+    };
+    // The company mailing address is the first non-empty line after the
+    // "Company Profile" H1 block that contains a comma+ZIP-ish pattern,
+    // sitting just before the "Show All" / section toggles.
+    let address = '';
+    const cpIdx = lines.map((l, i) => ({ l, i }))
+      .filter(o => o.l === 'Company Profile').map(o => o.i);
+    const startAt = cpIdx.length ? cpIdx[cpIdx.length - 1] : 0;
+    for (let i = startAt + 1; i < Math.min(lines.length, startAt + 12); i++) {
+      const l = lines[i];
+      if (!l || l === '-' || /^Show All$/i.test(l)) continue;
+      if (/,\s*[A-Z]{2}\s+\d{5}(-\d{4})?/.test(l)) { address = l; break; } // "…, ST 99999"
+    }
+    return {
+      address,
+      dba: after('DBA Name:') || (txt.match(/DBA Name:\s*([^\n]+)/) || [, ''])[1].trim(),
+      caNum: after('CA #'),
+      eid: after('Company ID (EID)'),
+      naic: after('NAIC'),
+      status: after('Status'),
+      categoryType: after('Category Type'),
+      stateName: after('State Name'),
+      origin: after('Origin'),
+      naicGroupName: after('NAIC Group Name'),
+    };
+  });
+}
+
+// Split "123 Main St, Suite 4, City, ST 99999-1234" → {street, city, state, zip}
+function splitAddress(addr) {
+  if (!addr) return { street: '', city: '', st: '', zip: '' };
+  const m = addr.match(/^(.*),\s*([^,]+),\s*([A-Z]{2})\s+(\d{5})(?:-\d{4})?\s*$/);
+  if (!m) return { street: addr, city: '', st: '', zip: '' };
+  return { street: m[1].trim(), city: m[2].trim(), st: m[3].trim(), zip: m[4].trim() };
+}
+
+// ============================================================================
+async function run() {
+  const browser = await chromium.launch({ headless: true });
+  const context = await browser.newContext();
+  context.setDefaultTimeout(60000);
+  const rows = [];
+  const seen = new Set(); // dedupe by eid|naic
+
+  const writeCsv = () => {
+    const header = COLS.join(',');
+    const body = rows.map(r => COLS.map(c => cell(r[c])).join(',')).join('\n');
+    writeFileSync(OUT, header + '\n' + body + '\n');
+    console.error(`  → checkpoint: wrote ${rows.length} rows to ${OUT}`);
+  };
+
+  // 1. TITLE INSURERS (Lines of Insurance = Title) — the primary, reliable universe.
+  //    List + harvest details + CHECKPOINT the CSV *before* touching the crash-prone
+  //    UTC universe, so a later context crash can never lose these.
+  console.error('[list 1/2] Title insurers (f?p=144:10)…');
+  const lp = await context.newPage();
+  await lp.goto(P10, { waitUntil: 'networkidle' });
+  await setSelect(lp, 'P10_LINES_OF_INS', 'Title');
+  await setSelect(lp, 'P10_SORT_TYPE', 'Alphabetical');
+  await sleep(400);
+  await lp.click('#B451607695751751776'); // Get Companies
+  await clickConfirmOK(lp);
+  const insurers = await collectAllPages(lp);
+  console.error(`      → ${insurers.length} title insurers listed`);
+  try { await lp.close(); } catch {}
+  console.error('[detail 1/2] Title insurers…');
+  await harvestDetails(context, insurers, 'Title Insurer', rows, seen);
+  writeCsv(); // insurers are now SAFE on disk regardless of what UTCs do
+  try { await browser.close(); } catch {}
+
+  // 2. UNDERWRITTEN TITLE COMPANIES — pure upside, isolated in its OWN fresh browser
+  //    so its known context-crashing APEX pagination can't touch the insurers above.
+  try {
+    const b2 = await chromium.launch({ headless: true });
+    const c2 = await b2.newContext(); c2.setDefaultTimeout(60000);
+    const up = await c2.newPage();
+    console.error('[list 2/2] Underwritten Title Companies (f?p=144:20)…');
+    await up.goto(P20, { waitUntil: 'networkidle' });
+    await setSelect(up, 'P20_FILTER', 'Underwritten Title Companies');
+    await sleep(400);
+    await up.click('#B452459515237587778'); // Get Entities
+    await clickConfirmOK(up);
+    const utcs = await collectAllPages(up);
+    console.error(`      → ${utcs.length} underwritten title companies listed`);
+    try { await up.close(); } catch {}
+    console.error('[detail 2/2] Underwritten Title Companies…');
+    await harvestDetails(c2, utcs, 'Underwritten Title Company', rows, seen);
+    writeCsv(); // rewrite with UTCs appended
+    try { await b2.close(); } catch {}
+  } catch (e) {
+    console.error(`      ! UTC phase failed (${String(e.message).split('\n')[0]}) — CSV already holds the ${rows.length} title insurers`);
+  }
+
+  // ---- write CSV -----------------------------------------------------------
+  const header = COLS.join(',');
+  const body = rows.map(r => COLS.map(c => cell(r[c])).join(',')).join('\n');
+  writeFileSync(OUT, header + '\n' + body + '\n');
+  console.error(`\nWROTE ${OUT} — ${rows.length} rows`);
+  console.error('columns:', COLS.join(','));
+  return rows;
+}
+
+// Walk every result page (Next pagination) collecting listing rows.
+async function collectAllPages(page) {
+  const all = [];
+  const seenNames = new Set();
+  let pageNo = 0;
+  while (true) {
+    pageNo++;
+    const listed = await readResultTable(page);
+    for (const l of listed) {
+      const key = l.name + '|' + l.eid;
+      if (!seenNames.has(key)) { seenNames.add(key); all.push(l); }
+    }
+    if (!(await hasNext(page)) || pageNo > 20) break;
+    await page.getByRole('link', { name: /^Next$/i }).first().click()
+      .catch(async () => { await page.click('button:has-text("Next")').catch(() => {}); });
+    await page.waitForLoadState('networkidle', { timeout: 60000 }).catch(() => {});
+    await sleep(1500);
+  }
+  return all;
+}
+
+// For each listing row, open its checksum-signed detail page (same session,
+// so the &cs= token is valid), parse fields, push a normalized row. Uses its
+// own page inside the shared context and reopens it if the page dies, so a
+// single crash never aborts the whole run.
+async function harvestDetails(context, listings, licenseType, rows, seen) {
+  let page = await context.newPage();
+  page.setDefaultTimeout(60000);
+  for (let i = 0; i < listings.length; i++) {
+    const item = listings[i];
+    const key = (item.eid || item.name) + '|' + (item.naic || '');
+    if (seen.has(key)) continue;
+    seen.add(key);
+
+    // Navigate to the checksum-signed detail page. The href is relative to
+    // /apex_extprd/ and carries a valid &cs= for THIS session.
+    const url = BASE + '/' + item.href;
+    let detail = {};
+    try {
+      if (page.isClosed()) { page = await context.newPage(); page.setDefaultTimeout(60000); }
+      await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 });
+      await sleep(600);
+      const bodyTxt = await page.evaluate(() => document.body.innerText.slice(0, 200));
+      if (/Session state protection|error has occurred/i.test(bodyTxt)) {
+        console.error(`      ! detail rejected for ${item.name} — recording listing-only`);
+        detail = {};
+      } else {
+        detail = await readDetail(page);
+      }
+    } catch (e) {
+      console.error(`      ! detail fetch failed for ${item.name}: ${e.message.slice(0, 60)}`);
+      detail = {};
+      try { if (page.isClosed()) { page = await context.newPage(); page.setDefaultTimeout(60000); } } catch {}
+    }
+
+    const a = splitAddress(detail.address || '');
+    const zip = a.zip;
+    const st = a.st || (detail.stateName ? usStateAbbr(detail.stateName) : '') || '';
+    rows.push({
+      source: 'ca_doi_title',
+      role: 'Title',
+      entity_name: item.name,
+      contact_name: '',                 // registry lists no personal contact for these entities
+      license_no: detail.caNum || '',   // CA license # from the profile; blank if not shown
+      license_type: licenseType,        // "Title Insurer" | "Underwritten Title Company"
+      license_status: detail.status || '',
+      address: a.street || '',
+      city: a.city || '',
+      county: '',                       // CDI gives no county; not inventing one
+      state: 'CA',                      // CA registry role; domicile kept in raw notes
+      zip: zip || '',
+      phone: '',                        // NOT exposed by source
+      website: '',                      // NOT exposed by source
+      market: market(zip),
+      commercial_flag: 'true',          // title entities serve CRE
+      within_300mi: isSoCal(zip) ? 'true' : 'false',
+      source_url: url,
+    });
+    console.error(`      [${i + 1}/${listings.length}] ${item.name} — ${a.city || detail.stateName || '?'} ${zip || ''} ${detail.caNum ? '('+detail.caNum+')' : ''}`);
+    await sleep(POLITE_MS);
+  }
+  try { if (!page.isClosed()) await page.close(); } catch {}
+}
+
+// Minimal US state-name → abbr (only needed as a fallback; address parse is primary).
+function usStateAbbr(name) {
+  const M = { California:'CA', Oklahoma:'OK', 'New York':'NY', Florida:'FL', Texas:'TX',
+    Nebraska:'NE', Ohio:'OH', Pennsylvania:'PA', Illinois:'IL', Arizona:'AZ' };
+  return M[name] || '';
+}
+
+run().catch(e => { console.error('FATAL:', e); process.exit(1); });

← e44ad41 A2: AZ State Bar RE-attorney scraper (azbar member-directory  ·  back to Rentv Licensed Targets  ·  feat(scraper): ca_dre_pm — Property Management from DRE regi a27680f →