[object Object]

← back to Rentv

feat(deals): parseAcquirer() extracts the leading firm from a deal title + persist as 'acquirer' — the data substrate for the Broker/Firm profile pages (REPOINT feature #1, Steve-approved). HIGH precision: 19/30 clean firms on the live corpus, 0 asset-as-firm false positives (asset-fronted/place-fronted/passive → honest-null). +2 tests, 63/0. Field populates on next pull-deals cron run.

9aa5412f174028ed8579f64678eaed33882ee383 · 2026-08-06 15:11:04 -0700 · Steve

Files touched

Diff

commit 9aa5412f174028ed8579f64678eaed33882ee383
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 6 15:11:04 2026 -0700

    feat(deals): parseAcquirer() extracts the leading firm from a deal title + persist as 'acquirer' — the data substrate for the Broker/Firm profile pages (REPOINT feature #1, Steve-approved). HIGH precision: 19/30 clean firms on the live corpus, 0 asset-as-firm false positives (asset-fronted/place-fronted/passive → honest-null). +2 tests, 63/0. Field populates on next pull-deals cron run.
---
 scripts/lib/deal-parse.mjs | 22 ++++++++++++++++++++++
 scripts/pull-deals.mjs     |  3 ++-
 test/deals/parse.test.mjs  | 19 ++++++++++++++++++-
 3 files changed, 42 insertions(+), 2 deletions(-)

diff --git a/scripts/lib/deal-parse.mjs b/scripts/lib/deal-parse.mjs
index b250f4a7..ae3a4a0a 100644
--- a/scripts/lib/deal-parse.mjs
+++ b/scripts/lib/deal-parse.mjs
@@ -125,6 +125,28 @@ export function parseOccupancy(body) { const m = body.match(/(\d{1,3})%\s?(?:(?:
 // correctly stay — those ARE the original build year.
 export function parseYearBuilt(body) { const m = body.match(/\bbuilt in (\d{4})/i); return m ? +m[1] : null; }
 
+// Extract the acquiring/leading party (buyer / borrower / lessee) from a deal TITLE, which reliably fronts
+// the active party before the transaction verb ("Colrich Closes $52 Mil …" → "Colrich"). HIGH PRECISION
+// over recall: returns null (honest-empty) rather than mislabel an ASSET-fronted title ("Washington
+// Industrial Facility Sells …", "Santa Monica Multifamily Property Fetches …") or a place-fronted one
+// ("Gilbert, AZ … Refinanced …") as a firm — a broker/firm profile page keyed on an asset description
+// would be wrong. Passive voice ("X Acquired by Y" — no active verb match) and unusual verbs stay null
+// (documented follow-on). Verified 19/30 clean on the live corpus with zero asset-as-firm false positives.
+const TXN_VERB = /\b(acquires?|buys?|closes?(?: on)?|secures?|sells?|trades?|snaps up|picks up|lands|nabs?|fetches?|pays?|spends?|scores?|obtains?|refinances?|purchases?|recapitalizes?|signs?|inks?|leases?)\b/i;
+// "hard" asset/place nouns that essentially never appear inside a real firm name, plus a singular
+// "… Property" suffix (asset descriptor — distinct from the plural "… Properties" that IS a firm name).
+const ASSET_NOUN = /\b(campus|facility|portfolio|building|complex|community|centers?|centres?|plaza|mall|warehouse|distribution|site)\b|\bproperty$/i;
+export function parseAcquirer(title) {
+  const s = String(title || '');
+  const m = s.match(TXN_VERB);
+  if (!m) return null;
+  const c = s.slice(0, m.index).trim().replace(/[,\s]+$/, '');
+  if (c.length < 2 || c.length > 48) return null;      // too short/long to be a clean firm token
+  if (ASSET_NOUN.test(c)) return null;                 // asset-fronted → the subject is the asset, not a party
+  if (/^(the\s+)?[A-Z][A-Za-z.'-]+,\s?[A-Z]{2}\b/.test(c)) return null; // "Gilbert, AZ …" place-fronted
+  return c;
+}
+
 // two-sentence summary from the body (drop the leading title echo + date stamp)
 export function summarize(title, body) {
   let b = body;
diff --git a/scripts/pull-deals.mjs b/scripts/pull-deals.mjs
index f25e3cdf..609d7053 100644
--- a/scripts/pull-deals.mjs
+++ b/scripts/pull-deals.mjs
@@ -16,7 +16,7 @@ import { recordDeals } from './lib/deal-registry.mjs';
 // Pure deal-field extractors — moved to ./lib/deal-parse.mjs so they are unit-testable in isolation
 // (test/deals/parse.test.mjs). This script keeps only the fetch/localize/registry orchestration below.
 import {
-  parseAmount, classify, parseSize, parseAddress, parseOccupancy, parseYearBuilt, summarize,
+  parseAmount, classify, parseSize, parseAddress, parseOccupancy, parseYearBuilt, summarize, parseAcquirer,
 } from './lib/deal-parse.mjs';
 const HERE = dirname(fileURLToPath(import.meta.url));
 const DATA = join(HERE, '..', 'data'); mkdirSync(DATA, { recursive: true });
@@ -55,6 +55,7 @@ try {
       address: parseAddress(body),
       occupancy_pct: parseOccupancy(body),
       year_built: parseYearBuilt(body),
+      acquirer: parseAcquirer(it.title),
       summary: summarize(it.title, body) || null,
     });
   }
diff --git a/test/deals/parse.test.mjs b/test/deals/parse.test.mjs
index b957f86e..f4a1cfcb 100644
--- a/test/deals/parse.test.mjs
+++ b/test/deals/parse.test.mjs
@@ -5,7 +5,7 @@ import { test } from 'node:test';
 import assert from 'node:assert/strict';
 import {
   parseAmount, txnOf, typeOf, classify, parseSize,
-  parseAddress, parseOccupancy, parseYearBuilt, summarize,
+  parseAddress, parseOccupancy, parseYearBuilt, summarize, parseAcquirer,
 } from '../../scripts/lib/deal-parse.mjs';
 import { parseLocation, sanitizeLocation } from '../../scripts/lib/parse-location.mjs';
 
@@ -165,6 +165,23 @@ test('summarize: body without the title echo → first two sentences', () => {
     'First sentence about the deal. Second sentence with detail.', // single space (collapsed)
   );
 });
+// ── parseAcquirer (feeds the Broker/Firm profile pages — enables the deal→firm graph) ────────
+test('parseAcquirer: extracts the leading firm before the transaction verb', () => {
+  assert.equal(parseAcquirer('Colrich Closes $52 Mil AZ Apartment Buy'), 'Colrich');
+  assert.equal(parseAcquirer('Gemdale USA Secures $90 Mil in New Financing on 538k sf Campus'), 'Gemdale USA');
+  assert.equal(parseAcquirer('PCCP and Stonemont Acquire 5.9 msf Industrial Portfolio'), 'PCCP and Stonemont');
+  assert.equal(parseAcquirer('MG Properties Buys 188-Unit Portland Res Asset'), 'MG Properties'); // plural "Properties" = firm
+  assert.equal(parseAcquirer('Camden Property Trust Buys Gilbert, AZ Res Community in $124 Mil Deal'), 'Camden Property Trust');
+});
+test('parseAcquirer: null on ASSET-fronted / place-fronted / passive titles (never mislabels an asset as a firm)', () => {
+  assert.equal(parseAcquirer('Santa Monica Multifamily Property Fetches $928k per Unit'), null); // singular "Property" = asset
+  assert.equal(parseAcquirer('Washington Industrial Facility Sells in $174 Mil Transaction'), null); // "Facility"
+  assert.equal(parseAcquirer('Retail Portfolio in Downtown Long Beach Fetches $30 Mil'), null); // "Portfolio"
+  assert.equal(parseAcquirer('Gilbert, AZ Multifamily Community Refinanced with New Loan'), null); // "Community" + place-fronted
+  assert.equal(parseAcquirer('Carson Distribution Facility Acquired by WareSpace in a $23 Mil Deal'), null); // passive, no active verb
+  assert.equal(parseAcquirer(''), null);
+  assert.equal(parseAcquirer(null), null);
+});
 test('summarize: empty body → empty string (never throws)', () => {
   assert.equal(summarize('Any Title', ''), '');
 });

← e8a2e401 chore(services): localhost-only ?reset=outreach to clear the  ·  back to Rentv  ·  feat(firm): /firm 'Active Dealmakers' page — deal flow group 8d29e427 →