← back to Rentv Adintel
docs/ADVERTISER_RESEARCH_METHOD.md
106 lines
# RENTV Advertiser Intelligence — Research Method
How the tool classifies relationships, resolves entities, and researches LinkedIn /
public contacts. The rule throughout: **distinguish facts from guesses, and never label
a company an advertiser merely because it operates in CRE, appeared on a panel, or was
mentioned in an article.**
## 1. Classification system (§2)
Every company ↔ event/publication relationship carries exactly one status:
| Status | Meaning |
|--------|---------|
| `VERIFIED_ADVERTISER` | A dated public ad, eblast, sponsored placement, paid spotlight, paid video, advertorial, or direct internal RENTV record supports the claim. |
| `VERIFIED_CONFERENCE_SPONSOR` | An event page, sponsor grid, agenda, program, email, PDF, or official record explicitly calls the company a sponsor. |
| `VERIFIED_EXHIBITOR` | An official exhibitor directory or floor plan supports the claim. |
| `VERIFIED_MEDIA_PARTNER` | The event or publisher explicitly says "media partner." |
| `VERIFIED_CONTENT_PARTNER` | Supplies a report / research presentation / program content, but payment is not proven (e.g. CoStar Group). |
| `SPEAKER_OR_PANELIST_ONLY` | Participation is verified, but sponsorship/advertising is not. |
| `PAST_ADVERTISER` | Verified historical advertising with no recent sighting inside the recency window. |
| `LIKELY_PROSPECT` | Not proven as an advertiser, but supported by transparent fit factors. |
| `RESEARCH_NEEDED` | Evidence is incomplete or conflicting. |
| `DISQUALIFIED` | Duplicate, irrelevant, closed, noncommercial, suppressed, or prohibited source. |
The plain-English label for each status lives in `lib/types.js` (`STATUS_LABELS`) and is
shown on every record alongside its supporting evidence. The `VERIFIED_*` set drives the
Simple-View "Verified Only" switch.
### Panelist ≠ sponsor (hard rule, §6.16)
A `SPEAKER_OR_PANELIST_ONLY` record may not be promoted to a verified
sponsor/advertiser status without **separate sponsor evidence**. The classification
guard (`assertNotPanelistMislabeledAsSponsor(fromStatus, toStatus, hasSponsorEvidence)`)
throws on any such promotion when `hasSponsorEvidence` is false, and
`test/classification.test.js` locks it in. This is why the CoStar seed is a
content-partner and why the conference participants (CBRE, Colliers, Cushman & Wakefield,
Rexford Industrial, …) are seeded as `SPEAKER_OR_PANELIST_ONLY` / `LIKELY_PROSPECT` — not
sponsors.
## 2. Deterministic entity resolution (§12)
Match deterministically first, then generate candidates, then apply thresholds.
**Deterministic keys (tried first):**
- exact normalized domain,
- exact public business email domain,
- exact normalized phone + organization name,
- exact official identifier,
- exact normalized name + city/state,
- exact LinkedIn company URL from a permitted discovery source.
Names are normalized via `normalizeName()` in `lib/types.js` (lowercases, strips
articles and corporate suffixes like Inc./LLC/Group/Corp, collapses punctuation) so
"The Rockefeller Group, Inc." and "Rockefeller Group" resolve to the same key
`rockefeller`.
**Candidate generation:** normalized names, aliases, domains, addresses, phones,
geography, and event overlap.
**Thresholds:**
| Similarity | Action |
|------------|--------|
| `>= 0.97` | Auto-link **only** when no contradictory identifier exists. |
| `0.82 – 0.9699` | Human review (side-by-side merge screen). |
| `< 0.82` | Do not merge. |
Every merge is reversible with evidence, conflicts, and undo (`merge_candidates` /
`merge_audit`).
## 3. LinkedIn & public-contact research workflow (§14)
**Search-URL-only. Human-open. Never fetch.**
For each organization the tool generates permitted search-provider queries (or one-click
manual search URLs when no search API key is configured), e.g.:
```text
"{company}" commercial real estate California marketing
"{company}" Arizona commercial real estate sponsor
"{company}" media contact
site:{domain} newsroom "media contact"
site:linkedin.com/company "{company}"
site:linkedin.com/in (marketing OR communications OR partnerships OR "business development") "{company}" California
```
The `site:linkedin.com/...` queries produce a **search URL a human opens in a normal
browser** — the app never fetches LinkedIn (blocked by `fetch-guard`). From a result we
store only the URL, title/snippet (licensing permitting), the discovery query, the
discovery date, and a manual-review status.
**Contact role priority** (highest first, `CONTACT_ROLE_PRIORITY` in `lib/types.js`):
CMO → VP/SVP Marketing → Marketing Director → Communications/PR Director → Events /
Partnerships / Sponsorships Director → Business Development Director → Regional President
/ Market Leader → Managing Director / Principal → Public media contact → General company
contact.
**Contact verification** — for every phone/email we show the exact source, whether it is
organization-level or person-level, the observed date, the last verification date, the
confidence, and a report/correction control. We never infer a missing value.
**Per-record actions** (no auto-send, no auto-dial in the first release): Open Website,
Open Contact Page, Open Source, Open LinkedIn, Search LinkedIn, Copy Business Email, Copy
Business Phone, Add Note, Create Task.