Single-unit listings (condos, houses, individual units) now return real data.
These use a different page template — a bare <h1> inside div.delivery-address with city/state/ZIP in a following <h2>, and mainEntity.@type of Apartment rather than ApartmentComplex. The parsers only understood the multi-unit template, so such a listing produced a row with an empty propertyName, an empty address and state set to the breadcrumb literal "Condos". These listings are reachable through the filters like any other — one of three rows in a live bedrooms+rent+pets run was affected.
parse_property_name and parse_address_parts now handle both templates. Live: 276 Corey Rd Unit 12A now returns its name and 276 Corey Rd Unit 12A, Boston, MA 02135 with state: "Massachusetts".
- Safety net: a page that yields neither a property name nor an address now emits an uncharged row with
errorReason: "unsupported_page_template" instead of a charged, near-empty result row.
Fixed a page-validity bug that could silently drop qualifying properties.
Page validity was gated on id="propertyName", which Apartments.com emits at roughly 8% of the document — while the ld+json property blob carrying images, tours, pet policy, neighborhood and aggregateRating arrives as the last block, at ~93% (measured 9/9: 62,971 vs 545,728 of 588,971 bytes). The check could therefore pass on a page missing 37 KB of property data, yielding null fields non-deterministically.
The practical harm was on the guard filters: minRating / verifiedOnly / minSqft read fields from that late blob, so a half-rendered page made a guard reject a property that actually qualified — a silent drop the user could not see. Reproduced offline by truncating real captures: the old gate accepted the truncated page on 9/9, and minRating=3 wrongly rejected two genuinely qualifying properties (3.7 and 4.4).
- Property pages now require the ld+json property blob to be present and to parse. A substring check is not sufficient: the
ApartmentComplex and "mainEntity" tokens sit at the blob's start, so truncating at them leaves the tokens intact. Requiring a parse also keeps the gate type-agnostic for houses and condos.
- Search pages now additionally wait for the listing-card count to stop growing between polls. Cards stream in progressively (first at ~214 KB, last at ~469 KB of a 601 KB page), so a partly-rendered page could previously truncate discovery.
The unquoted-ratingValue fix from 0.1 was re-examined and is independent of this bug, not a symptom: against fully-rendered captures the old regex still finds 0 of 9 ratings while the current parser finds all 3 real ones. It stays as-is.
Initial release — filter-first Apartments.com scraper.
New capabilities
- Server-side filter-segment URL builder.
bedrooms, maxRent and petPolicy compile into Apartments.com's own filtered index path (/2-bedrooms-under-3000-pet-friendly-dog/), so the site filters its full inventory. Known-bad segments (min-X-max-Y → 404, singular studio → 404, 0-bedrooms → silent no-op) are rejected by the builder and reported in appliedFilters.rejected.
- Search-card pre-filter. Rent floor/ceiling, move-in specials, pets and required amenities are enforced against the 40-card search payload before any property-detail page is fetched.
- Filter telemetry + guard filters. Rows carry
matchedResultCount (Apartments.com's own match count for the filtered URL), filteredSearchUrl, searchPage and appliedFilters, plus post-fetch minRating / verifiedOnly / minSqft / minBaths guards for criteria with no URL segment.
Engine
- Replaced the static HTTP client with Playwright Firefox on Apify US Residential. The previous engine returned zero rows on live Apartments.com (0 clean pages in 120 exit-IP rolls); Akamai hard-403s non-Firefox TLS fingerprints and serves a JS interstitial to the rest.
- Block detection now keys on the interstitial's own markers (
sec-if-cpt-container) and on the presence of the expected result marker, instead of a page-length heuristic that the 2,490-byte interstitial passed.
- One browser context and one sticky exit IP are reused across the run; a fresh IP is rolled only when a page comes back blocked. Images, fonts and media are aborted at the route level.
Fixes carried over from the original actor
includeAllImages and includeVirtualTours now return real data instead of empty arrays.
rating now populates. The previous regex required a quoted structured-data value; Apartments.com publishes it unquoted inside aggregateRating, so the column was null even on rated properties. reviewCount added alongside it.
location.neighborhood now reads the authoritative structured-data neighborhood, with the breadcrumb value as fallback and neighborhoodSource recording which was used.
- Removed the dead
maxRetriesPerRequest input (read but never used) and the "auto-escalation" / "3 retry rounds" proxy claims, which the code never implemented.
requestDelay is now declared in the input schema instead of being a hidden tunable.
- A property that cannot be fetched or parsed now emits an uncharged typed error row instead of disappearing silently.
- The page cap is one number everywhere: 28, Apartments.com's own limit.