[object Object]

← back to Rentv 2026

PR intel: better editorial-contact extraction for news publications

ccbda3ed358228ffd44fdf5f58f9e6b5dcd40e12 · 2026-08-05 13:21:43 -0700 · Steve

- team-page discovery now finds mastheads / editorial-team / contributors
  pages (news sites have no /team page)
- TITLE_HINT recognizes editorial titles (editor/reporter/writer/publisher/
  columnist/correspondent) so publication staff are captured, not skipped
- headline guard: reject name/title pairs that read like article headlines
  (Hires/Names/Expands/... + $/sf/number patterns) and geographic/section
  words as names — kills the 'New York City / Arrow ... Hires' class of noise

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit ccbda3ed358228ffd44fdf5f58f9e6b5dcd40e12
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 5 13:21:43 2026 -0700

    PR intel: better editorial-contact extraction for news publications
    
    - team-page discovery now finds mastheads / editorial-team / contributors
      pages (news sites have no /team page)
    - TITLE_HINT recognizes editorial titles (editor/reporter/writer/publisher/
      columnist/correspondent) so publication staff are captured, not skipped
    - headline guard: reject name/title pairs that read like article headlines
      (Hires/Names/Expands/... + $/sf/number patterns) and geographic/section
      words as names — kills the 'New York City / Arrow ... Hires' class of noise
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 src/pr/adapters/website.js | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/src/pr/adapters/website.js b/src/pr/adapters/website.js
index cf5db05e..080cf0dd 100644
--- a/src/pr/adapters/website.js
+++ b/src/pr/adapters/website.js
@@ -7,7 +7,7 @@ const { normalizeDomain, normalizeEmail } = require('../lib/normalize');
 
 const PAGE_HINTS = {
   press: /news(room)?|press|media(-|\s)?(center|centre|room|contact|kit)|announcements/i,
-  team: /team|people|leadership|our-people|professionals|staff|about-us\/(team|people|leadership)|management/i,
+  team: /masthead|editorial[\s-]?(team|staff|board)|contributors|our-writers|meet-the-team|team|people|leadership|our-people|our-team|professionals|staff|who-we-are|about-us\/(team|people|leadership|staff|masthead)|management/i,
   contact: /contact/i,
   // Media/PR outreach pages (publications, trade media, association media):
   submission: /submit|press[\s-]?release|news[\s-]?tip|share[\s-]?(your[\s-]?)?news|send[\s-]?(us[\s-]?)?news|contribute|editorial[\s-]?submission|for[\s-]?(the[\s-]?)?press|story[\s-]?ideas?/i,
@@ -36,7 +36,7 @@ function extractLinks(html, baseUrl) {
 }
 
 /** Titles worth extracting from team pages (kept intentionally broad; classify later). */
-const TITLE_HINT = /officer|director|president|principal|partner|manager|vp|vice president|head of|chief|founder|lead|communications|marketing|media|escrow|lending|relations/i;
+const TITLE_HINT = /officer|director|president|principal|partner|manager|vp|vice president|head of|chief|founder|lead|communications|marketing|media|escrow|lending|relations|editor|editorial|reporter|writer|journalist|correspondent|columnist|publisher|contributor|newsroom/i;
 
 /** Extract (name, title) candidate pairs from a team/leadership page. Conservative:
  *  requires a plausible Name-Case name adjacent to a title-looking string. */
@@ -53,14 +53,19 @@ function extractPeople(html) {
   // Name-Case headings that are NOT names ("Crisis Communications", "Dream Job") — any
   // word from this list disqualifies the candidate. Real ambiguity still lands in
   // needs_verification for human review; this just cuts the obvious junk.
-  const NOT_NAME = /\b(job|jobs|career|careers|communication|communications|media|crisis|digital|social|marketing|advisor|advisors|strategy|strategies|services|solutions|relations|estate|group|team|about|contact|office|offices|leadership|management|partners|clients|projects|news|insights|events|home|search|menu|privacy|policy|our|the|and)\b/i;
+  const NOT_NAME = /\b(job|jobs|career|careers|communication|communications|media|crisis|digital|social|marketing|advisor|advisors|strategy|strategies|services|solutions|relations|estate|group|team|about|contact|office|offices|leadership|management|partners|clients|projects|news|insights|events|home|search|menu|privacy|policy|our|the|and|york|city|angeles|francisco|diego|health|care|county)\b/i;
+  // On news/publication sites a "title" line is often an article HEADLINE, not a role
+  // (e.g. "Arrow Real Estate Advisors Hires Jeff Wesley"). Reject pairs whose name or
+  // title reads like a headline — a real job title never contains these verbs.
+  const HEADLINE = /\b(hires?|names?|appoints?|promotes?|announces?|expands?|launches?|acquires?|sells?|buys?|opens?|joins?|closes?|signs?|completes?|leases?|inks?|secures?|welcomes?|taps?|raises?|unveils?|debuts?|breaks? ground)\b/i;
+  const isHeadline = (n, t) => HEADLINE.test(n) || HEADLINE.test(t) || /\$\d|\d{3,}|\bsf\b|square feet/i.test(t);
   // single-line variant: "Jane Smith, Senior Vice President" / "Jane Smith – CFO" /
   // "Jane Smith | Chief Credit Officer" — common on small-bank officer pages (cycle 4:
   // 36 team pages yielded 0 because name+title share one line).
   const INLINE_RE = /^([A-Z][a-z]+(?:\s+[A-Z]\.?)?(?:\s+[A-Z][a-z'’-]+){1,2})\s*[,|–—-]\s+(.{3,90})$/;
   for (let i = 0; i < lines.length; i++) {
     const inline = lines[i].match(INLINE_RE);
-    if (inline && !NOT_NAME.test(inline[1]) && TITLE_HINT.test(inline[2])) {
+    if (inline && !NOT_NAME.test(inline[1]) && TITLE_HINT.test(inline[2]) && !isHeadline(inline[1], inline[2])) {
       people.push({ full_name: inline[1], exact_title: inline[2].trim() });
       if (people.length >= 60) break;
       continue;
@@ -71,7 +76,7 @@ function extractPeople(html) {
     for (let j = 1; j <= 2 && i + j < lines.length; j++) {
       const cand = lines[i + j];
       if (cand.length > 90 || NAME_RE.test(cand)) break;
-      if (TITLE_HINT.test(cand)) { people.push({ full_name: nm[1], exact_title: cand }); break; }
+      if (TITLE_HINT.test(cand) && !isHeadline(nm[1], cand)) { people.push({ full_name: nm[1], exact_title: cand }); break; }
     }
     if (people.length >= 60) break; // sane cap per page
   }

← f6d63f54 Audience: surface the 2,162-contact pr-intelligence CRM (was  ·  back to Rentv 2026  ·  Audience: CRM contacts drill into their pr-intelligence pers 8c7c2767 →