Filter Saved Listings by Features in Their Descriptions
Search the description text of properties you already saved for features like a pool or a home office, quote the evidence for every match, and treat each hit as a lead for manual review.
Some features only live in the description
Structured fields cover the basics: beds, baths, living area, lot size, year built, home type. The things buyers and renters often ask about first, such as a pool, a fenced yard, a home office, solar panels or a finished basement, usually appear only in the listing description written by the agent or landlord. The property response returns that text as description.
This guide searches the descriptions of properties you already collected and saved. It does not search the market for homes with a pool; it narrows your own shortlist. For every match it quotes the surrounding sentence, so a reviewer can judge in seconds whether the listing actually says what the keyword suggests.
Keyword matches are leads, not facts
Listing text is marketing copy. It mentions things that are nearby rather than on the property, things that were removed, and things the agent is careful to say do not exist. A naive filter for the word pool will match a community pool, no pool, pool table and close to the public pool. The word office matches a home office, a post office and an office park down the road.
Two habits keep a text filter honest. First, never output a bare yes or no; output the quote that triggered the match. Second, label every result as a lead. The reviewer, not the regular expression, decides whether a feature is real, and the listing photos and a visit confirm it.
| Result label | Meaning | Next step |
|---|---|---|
| mentioned, verify | A term matched with no qualifier just before it | Read the quote, then check photos or ask |
| qualified mention, read carefully | A word like no, without, community or nearby comes just before the term | Usually not an on-property feature; read the quote |
| not mentioned | No pattern matched the description | Not evidence that the feature is absent |
| no description to search | The lookup failed or returned no description | Check the source page directly |
Start from a saved snapshot
The script reads a snapshot file of full property responses: an exported_at time and a list of rows with your id and the API's ok, data, meta and error. Any collector that saves full responses works, including the batch collectors in the other build guides. Searching a saved file is free and repeatable: you can try new terms as often as you like without another lookup.
Keep the collection time with the results. Descriptions change when a listing is updated, and a match from last month may no longer be in the current text. Off-market homes can still carry a description from a past listing, so check the status before treating a match as current.
The filter script
Each search term maps to a small list of patterns, so garage also finds carport and home office also finds den and study. Patterns use word boundaries, which keeps pool from matching spool and den from matching garden. Matching ignores case.
Before each match, the script looks at a short window of preceding text for qualifiers such as no, without, community, shared, nearby and walk to. A qualifier does not delete the match. It changes the label to qualified mention, because the right call depends on the sentence. That is also why the evidence column exists.
Rows with no description are reported as such for every term, instead of silently disappearing. The output is a CSV with one row per match, and the evidence cell is protected against spreadsheet formula injection.
"""filter_descriptions.py: search saved listing descriptions and quote the evidence.
Usage: python filter_descriptions.py snapshot.json pool garage "home office" > leads.csv
Matches are leads for manual review, not verified features.
"""
import csv, json, re, sys
# Phrases that usually describe the same feature. Extend for your own market.
SYNONYMS = {
"pool": [r"pool", r"swimming pool"],
"garage": [r"garage", r"carport"],
"home office": [r"home office", r"office", r"den", r"study"],
"fenced yard": [r"fenced(?:[- ]in)? (?:back)?yard", r"privacy fence"],
"solar": [r"solar(?: panels?)?"],
}
NEGATORS = re.compile(r"\b(?:no|not|without|never|lacks?|none|community|shared|nearby|near|close to|walk to)\W+(?:\w+\W+){0,2}$", re.I)
def patterns(term):
return [re.compile(rf"\b{p}\b", re.I) for p in SYNONYMS.get(term.lower(), [re.escape(term)])]
def quote(text, start, end, pad=70):
a, b = max(0, start - pad), min(len(text), end + pad)
return ("..." if a else "") + " ".join(text[a:b].split()) + ("..." if b < len(text) else "")
def search(snapshot, terms):
for row in snapshot["rows"]:
d, meta = row.get("data") or {}, row.get("meta") or {}
text = d.get("description")
if not row.get("ok") or not text:
for term in terms:
yield dict(id=row["id"], term=term, result="no description to search", evidence="",
detail_url=d.get("detailUrl"), fetched_at=meta.get("fetched_at"))
continue
for term in terms:
hits = [m for p in patterns(term) for m in p.finditer(text)]
if not hits:
yield dict(id=row["id"], term=term, result="not mentioned", evidence="",
detail_url=d.get("detailUrl"), fetched_at=meta.get("fetched_at"))
continue
last_end = -1
for m in sorted(hits, key=lambda m: (m.start(), -m.end())):
if m.start() < last_end: # "swimming pool" and "pool" are one mention
continue
last_end = m.end()
qualified = NEGATORS.search(text[max(0, m.start() - 40):m.start()])
yield dict(id=row["id"], term=term,
result="qualified mention, read carefully" if qualified else "mentioned, verify",
evidence=quote(text, m.start(), m.end()),
detail_url=d.get("detailUrl"), fetched_at=meta.get("fetched_at"))
if __name__ == "__main__":
snapshot = json.load(open(sys.argv[1], encoding="utf-8"))
w = csv.DictWriter(sys.stdout, fieldnames=["id", "term", "result", "evidence", "detail_url", "fetched_at"])
w.writeheader()
for r in search(snapshot, sys.argv[2:]):
if r["evidence"].startswith(("=", "+", "-", "@")): # spreadsheet formula guard
r["evidence"] = "'" + r["evidence"]
w.writerow(r)
Read the results
Run the script with your snapshot and the terms you care about, then open leads.csv in a spreadsheet. Sort by term and result. Read every mentioned row's quote, and every qualified row's quote too: a qualifier sometimes describes a different feature in the same sentence.
Below are illustrative rows that show each label. The second and third rows are why quotes matter: both contain the word pool, and neither describes a pool on the property.
python filter_descriptions.py snapshot.json pool garage "home office" > leads.csv
id,term,result,evidence
home-a,pool,"mentioned, verify","...entertain in the backyard with a heated pool and covered patio. Two-car garage..."
home-b,pool,"qualified mention, read carefully","...steps from the community pool and walking trails..."
home-c,pool,"qualified mention, read carefully","...low-maintenance yard with no pool to maintain..."
home-d,pool,not mentioned,
home-e,pool,no description to search,
# illustrative rows; the real file also has detail_url and fetched_atKnow the limits of text search
Silence is not absence. Many listings with a garage never mention it, and some descriptions are short or missing. Use not mentioned as a reason to look, not as a reason to drop a property. Where a structured field exists, prefer it over text: beds, baths, livingArea, lotSize and homeType come back as their own fields, and the description is a supplement to them.
Language varies by region and by agent. Extend the synonym lists with phrases you see in your own market, and add qualifiers that cause false matches. Keep the list in version control so the reasoning behind each pattern stays visible.
Finally, the description belongs to a listing that someone else wrote. Quote it for internal review, link to the source page, and avoid republishing long passages on a public site.
- Quote evidence for every match.
- Label results as leads and confirm features with photos or a visit.
- Treat not mentioned as unknown.
- Keep collection times, and rerun on a fresh snapshot before decisions.
- Prefer structured fields when the response has them.
Where to go next
Text filters pair well with a visual review. Load the shortlist into a dashboard, filter it to the leads you confirmed and look at each listing before shortlisting further. If photos are part of your review, the listing photos guide explains how the photo links work and how to use them responsibly.
Sources and documentation
Product details checked against the linked documentation on October 22, 2026. Code and sample inputs are illustrative unless stated otherwise.
- PropertiesAPI property fields, including description
- Python regular expressions: word boundaries, flags and finditer
- OWASP: CSV injection
Written by PropertiesAPI, an independent property data service. We are not affiliated with Zillow. Source data may be incomplete or change over time.