The agent was offline this day due to model credit limits, so this portion was done directly with Claude.
The problem
A striping business sells to whoever controls the lot, usually the property owner. The CRM had business names and addresses but no ownership data. Without that field, owner-operators, tenants, and property managers were mixed together.
The data source
Collin County parcel data from TNRIS (Texas Natural Resources Information System), downloaded from the StratMap Land Parcels collection. The same dataset was later expanded to Dallas, Denton, and Tarrant counties. The export includes:
| Column | Description |
|---|---|
prop_id | Unique parcel identifier |
situs_addr | Property street address |
owner_name | Name on the deed |
land_use | Commercial, retail, church, etc. |
acres | Lot size |
market_value | County assessed value |
year_built | Year of construction |
SQLite table
sqlCREATE TABLE parcels (
prop_id INTEGER PRIMARY KEY,
situs_addr TEXT,
owner_name TEXT,
land_use TEXT,
acres REAL,
market_value INTEGER,
year_built INTEGER,
city TEXT
);
CREATE INDEX idx_parcels_addr ON parcels (situs_addr);
CREATE INDEX idx_parcels_city ON parcels (city);
Matching prospects to parcels
Each CRM prospect gets looked up by street address with fuzzy matching:
pythondef parcel_lookup(street: str, city: str, db_path: str) -> dict | None:
conn = sqlite3.connect(db_path)
cur = conn.cursor()
street_norm = normalize_street(street)
cur.execute("""
SELECT prop_id, owner_name, situs_addr, acres, market_value, year_built, land_use
FROM parcels
WHERE situs_addr LIKE ? AND city = ?
LIMIT 1
""", (f"%${street_norm}%", city.upper()))
row = cur.fetchone()
conn.close()
if not row:
return None
keys = ["prop_id", "owner_name", "situs_addr", "acres", "market_value", "year_built", "land_use"]
return dict(zip(keys, row))
Ownership classification
Comparing parcel owner name to business name:
- owner_operator: names match. Direct sale, highest priority.
- tenant: parcel exists, different owner. Wrong contact.
- property_manager: no match or known PM company. Different pitch needed.
Token overlap matching handles formatting differences:
pythondef names_match(owner_name: str, business_name: str) -> bool:
def tokens(s):
s = s.upper()
for stop in ("LLC", "INC", "LP", "THE", "OF", "AND", "&", "CO", "CORP"):
s = s.replace(stop, "")
return set(s.split())
ow = tokens(owner_name)
biz = tokens(business_name)
if not ow or not biz:
return False
overlap = ow & biz
return len(overlap) / max(len(ow), len(biz)) >= 0.6
Result
Owner-operators rose to the top of the call queue:
| status | lead_type | meaning |
|---|---|---|
call_next | owner_operator | Business owns the lot, call first |
new | tenant | Doesn't control the lot |
new | property_manager | Unconfirmed, research first |
Later, the parcel table moved from local SQLite to D1 behind a Worker endpoint at GET /api/parcels/search.
