← back to Rentv 2026
auto-save: 2026-08-05T12:42:02 (6 files) — data/markets.json data/news.json data/ticker.json src/pr/index.js src/pr/lib/geo.js
eb29d4a12a8dcc970f8027279ecb17b665aa2179 · 2026-08-05 12:42:03 -0700 · Steve Abrams
Files touched
M data/markets.jsonM data/news.jsonM data/ticker.jsonM src/pr/index.jsM src/pr/lib/geo.jsM src/pr/lib/taxonomy.js
Diff
commit eb29d4a12a8dcc970f8027279ecb17b665aa2179
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 5 12:42:03 2026 -0700
auto-save: 2026-08-05T12:42:02 (6 files) — data/markets.json data/news.json data/ticker.json src/pr/index.js src/pr/lib/geo.js
---
data/markets.json | 16 ++++++++--------
data/news.json | 2 +-
data/ticker.json | 8 ++++----
src/pr/index.js | 22 +++++++++++++++-------
src/pr/lib/geo.js | 6 +++++-
src/pr/lib/taxonomy.js | 26 +++++++++++++++++++++++++-
6 files changed, 58 insertions(+), 22 deletions(-)
diff --git a/data/markets.json b/data/markets.json
index 423893b0..74d064a2 100644
--- a/data/markets.json
+++ b/data/markets.json
@@ -1,5 +1,5 @@
{
- "fetched_at": "2026-08-05T18:54:15.031Z",
+ "fetched_at": "2026-08-05T19:34:16.689Z",
"source": "Yahoo Finance — live (CBOE Treasury yields, Dow Jones/MSCI REIT indices)",
"curve": [
{
@@ -16,7 +16,7 @@
},
{
"label": "10-Year",
- "value": "4.61%",
+ "value": "4.62%",
"change_bps": -1,
"dir": "down"
},
@@ -30,20 +30,20 @@
"spreads": [
{
"label": "30Y – 10Y (curve)",
- "value": "+55 bps",
+ "value": "+56 bps",
"dir": "up"
},
{
"label": "10Y – 3M (curve)",
- "value": "+88 bps",
+ "value": "+89 bps",
"dir": "up"
}
],
"reits": [
{
"label": "DJ U.S. Real Estate",
- "value": "394.61",
- "change_pct": -0.37,
+ "value": "395.14",
+ "change_pct": -0.24,
"dir": "down"
},
{
@@ -54,8 +54,8 @@
},
{
"label": "Vanguard REIT ETF (VNQ)",
- "value": "$98.55",
- "change_pct": -0.52,
+ "value": "$98.67",
+ "change_pct": -0.4,
"dir": "down"
}
],
diff --git a/data/news.json b/data/news.json
index e99f878e..52db4e7b 100644
--- a/data/news.json
+++ b/data/news.json
@@ -1,6 +1,6 @@
{
"source": "rentv.com — live",
- "fetched_at": "2026-08-05T18:54:14.461Z",
+ "fetched_at": "2026-08-05T19:34:16.461Z",
"count": 30,
"items": [
{
diff --git a/data/ticker.json b/data/ticker.json
index d8d7ba5d..65af0ab9 100644
--- a/data/ticker.json
+++ b/data/ticker.json
@@ -1,5 +1,5 @@
{
- "fetched_at": "2026-08-05T18:54:15.001Z",
+ "fetched_at": "2026-08-05T19:34:16.662Z",
"la_office_vac": {
"label": "LA OFFICE VAC",
"value": "23.1%",
@@ -14,14 +14,14 @@
},
"cre_index": {
"label": "CRE INDEX",
- "value": "4,147",
- "change_pct": -0.37,
+ "value": "4,153",
+ "change_pct": -0.24,
"dir": "down",
"source": "Dow Jones U.S. Real Estate"
},
"ten_yr": {
"label": "10-YR",
- "value": "4.61%",
+ "value": "4.62%",
"change_bps": -1,
"dir": "down",
"source": "CBOE 10-Yr (^TNX)"
diff --git a/src/pr/index.js b/src/pr/index.js
index d66547bf..1fab610f 100644
--- a/src/pr/index.js
+++ b/src/pr/index.js
@@ -24,7 +24,7 @@ const providers = require('./services/email-providers');
const jobs = require('./jobs');
const adapters = require('./adapters');
const geo = require('./lib/geo');
-const { ORG_TYPES, ASSET_CLASSES } = require('./lib/taxonomy');
+const { ORG_TYPES, ASSET_CLASSES, UNIVERSAL_TYPES, MAJOR_ONLY_TYPES } = require('./lib/taxonomy');
const PAGES = ['index', 'research', 'organizations', 'people', 'review', 'campaigns', 'letters', 'inbox', 'tasks', 'sources', 'runs', 'settings'];
@@ -125,13 +125,21 @@ module.exports = function mountPR(app, { adminOnly, sendPage }) {
const gate = await settings.get('california_gate', { passed: false });
const azUnlocked = await settings.arizonaUnlocked();
const coverage = await settings.get('coverage_report_CA', null);
- // CA completion: verified CA orgs vs all CA orgs + metro×category cells with any coverage
+ // CA completion: metro×category cells with coverage, over only REALISTICALLY-FILLABLE cells.
+ // A cell counts iff its category is 'universal' (any metro) or 'major' in a MAJOR metro — this
+ // is the tier model (taxonomy.ORG_TYPE_TIER) that stops the denominator counting specialist
+ // firms in secondary metros where they don't exist (Steve 8/5: "fix the denominator").
const caCells = await db.one(
- `SELECT count(DISTINCT (m, organization_type))::int AS covered
- FROM pr_organizations, unnest(metros) m
- WHERE 'CA'=ANY(state_presence) AND lifecycle_status NOT IN ('duplicate','archived')
- AND organization_type <> ALL($1::text[])`, [excludeTypes]);
- const totalCells = geo.CA_METROS.length * (ORG_TYPES.length - excludeTypes.length);
+ `SELECT count(*)::int AS covered FROM (
+ SELECT DISTINCT m, organization_type
+ FROM pr_organizations, unnest(metros) m
+ WHERE 'CA'=ANY(state_presence) AND lifecycle_status NOT IN ('duplicate','archived')
+ AND ( organization_type = ANY($1::text[])
+ OR (organization_type = ANY($2::text[]) AND m = ANY($3::text[])) )
+ ) fillable_covered`,
+ [UNIVERSAL_TYPES, MAJOR_ONLY_TYPES, geo.MAJOR_METRO_KEYS]);
+ const totalCells = UNIVERSAL_TYPES.length * geo.CA_METROS.length
+ + MAJOR_ONLY_TYPES.length * geo.MAJOR_METRO_KEYS.length;
res.json({
california: {
organizations: orgTotals.ca,
diff --git a/src/pr/lib/geo.js b/src/pr/lib/geo.js
index 281e3a4c..c722af3e 100644
--- a/src/pr/lib/geo.js
+++ b/src/pr/lib/geo.js
@@ -29,6 +29,10 @@ const AZ_METROS = [
const STATES = { CA: { label: 'California', metros: CA_METROS }, AZ: { label: 'Arizona', metros: AZ_METROS } };
+// Major CA metros — the coverage tier model expects 'major'-tier categories (finance/PR/
+// proptech) only here, not in secondary metros (Sacramento, Inland Empire, Central Coast/Valley).
+const MAJOR_METRO_KEYS = ['la', 'oc', 'san-diego', 'bay-area'];
+
function metrosFor(state) { return (STATES[state] || {}).metros || []; }
function metroLabel(state, key) {
const m = metrosFor(state).find((x) => x.key === key);
@@ -47,4 +51,4 @@ function geoPriority(state, metroKey) {
return Math.max(30, 100 - Math.round((i / Math.max(list.length - 1, 1)) * 60));
}
-module.exports = { CA_METROS, AZ_METROS, STATES, metrosFor, metroLabel, metroForCounty, geoPriority };
+module.exports = { CA_METROS, AZ_METROS, MAJOR_METRO_KEYS, STATES, metrosFor, metroLabel, metroForCounty, geoPriority };
diff --git a/src/pr/lib/taxonomy.js b/src/pr/lib/taxonomy.js
index 00401556..ace20600 100644
--- a/src/pr/lib/taxonomy.js
+++ b/src/pr/lib/taxonomy.js
@@ -42,6 +42,30 @@ const ORG_TYPES = [
const ORG_TYPE_KEYS = ORG_TYPES.map(([k]) => k);
const ORG_TYPE_LABEL = Object.fromEntries(ORG_TYPES);
+// ── Coverage tier model (Steve 8/5: "fix the denominator") ──────────────────
+// The CA completion metric counts (metro × category) cells. Counting EVERY category in
+// EVERY metro over-inflates the denominator, because specialist finance/PR/proptech firms
+// don't realistically exist in every secondary metro. So each category is tiered:
+// 'universal' — realistically present in every CA metro (counts against all 8 metros)
+// 'major' — concentrates in the big metros (counts only against MAJOR_METRO_KEYS)
+// 'excluded' — banks/credit-unions: out of the PR-outreach denominator entirely
+// Denominator = universal×(all metros) + major×(major metros). Edit here to re-tune.
+const ORG_TYPE_TIER = {
+ brokerage: 'universal', developer_owner: 'universal', asset_property_mgmt: 'universal',
+ title_insurer: 'universal', title_agency: 'universal', escrow_company: 'universal',
+ commercial_escrow_dept: 'universal', law_firm: 'universal', accounting_consulting: 'universal',
+ appraiser: 'universal', insurance_risk: 'universal', architecture_firm: 'universal',
+ engineering_firm: 'universal', general_contractor: 'universal', construction_mgmt: 'universal',
+ economic_development: 'universal', municipal_re: 'universal', association: 'universal',
+ reit: 'major', facility_mgmt: 'major', commercial_mortgage_bank: 'major', debt_equity_broker: 'major',
+ construction_lender: 'major', bridge_lender: 'major', private_lender: 'major', cmbs_structured: 'major',
+ life_co_lender: 'major', exchange_1031: 'major', proptech: 'major', pr_agency: 'major',
+ marketing_agency: 'major', public_affairs: 'major', trade_publication: 'major',
+ bank: 'excluded', credit_union: 'excluded',
+};
+const UNIVERSAL_TYPES = ORG_TYPE_KEYS.filter((k) => ORG_TYPE_TIER[k] === 'universal');
+const MAJOR_ONLY_TYPES = ORG_TYPE_KEYS.filter((k) => ORG_TYPE_TIER[k] === 'major');
+
const ASSET_CLASSES = [
'office', 'industrial', 'logistics', 'retail', 'multifamily', 'hospitality',
'medical_office', 'life_science', 'senior_housing', 'student_housing',
@@ -153,4 +177,4 @@ function roleRelevance(department, normalized_role) {
return base;
}
-module.exports = { ORG_TYPES, ORG_TYPE_KEYS, ORG_TYPE_LABEL, ASSET_CLASSES, classifyTitle, roleRelevance };
+module.exports = { ORG_TYPES, ORG_TYPE_KEYS, ORG_TYPE_LABEL, ORG_TYPE_TIER, UNIVERSAL_TYPES, MAJOR_ONLY_TYPES, ASSET_CLASSES, classifyTitle, roleRelevance };
← d1b78915 Content Studio: Boomer voice + video buttons — narrate the g
·
back to Rentv 2026
·
Boomer voice fix: re-clone from verified 7/31 VO (VOICE_ID c 9ea3fd53 →