# Changelog of Pige Immobilière 🇫🇷 : SeLoger + PAP + Logic-Immo dédupliqués (`tagadanar/french-real-estate-pige`) Actor

- **URL**: https://apify.com/tagadanar/french-real-estate-pige/changelog.md
- **Full Actor documentation**: https://apify.com/tagadanar/french-real-estate-pige.md

## Changelog

### 2026-09-23

- Fixed: a PAP search whose location code PAP has retired was silently
  redirected to PAP's nationwide listing, and the run shipped (and charged)
  flats from anywhere in France. PAP's pages past the last real one did the
  same: they fall back to page 1, which carries a few rotating ads, so the run
  kept reading up to 40 copies of it. Every PAP page is now checked against the
  address it actually landed on. A search that lost its location is skipped and
  named in the status, and pagination stops at the real last page.
- Fixed: the example search in the input form was one of those retired URLs.
  It now points at a working Marseille search.
- Faster, cheaper scheduled runs: a search is no longer read to the end when it
  has stopped producing anything you can receive. Three pages in a row with no
  new listing matching your filters (for example, only agency ads when you asked
  for private sellers) ends that search. A quiet monitor re-run of the example
  search went from 447 s to 43 s.
- With `withPhoneOnly`, PAP detail pages are fetched for at most 1.5×
  `maxResults` listings rather than every candidate.
- The run's own status message is no longer occasionally replaced by the
  crawler's generic "Finished! Total N requests" line.
- Pricing notice: from **2026-10-08** the per-run start charge goes from
  $0.001 to $0.005 (per GB of run memory, so $0.005 at the default 1 GB).
  The per-listing price is unchanged.

### 2026-08-29 (later)

- Fixed: PAP listings came back with `energyClass: null` even when the ad
  showed a DPE. The PAP parser bundled here had fallen behind the standalone
  PAP actor, which learned to read the DPE scale off the detail page on
  2026-08-19. Both PAP modules are back in sync, so a PAP row now carries its
  energy class like a SeLoger or Logic-Immo one.
- Removed the PAP "contact name" extraction. The selector it used was picking
  up page furniture rather than the owner's name, so it was either empty or
  wrong. Phone numbers are unaffected.
- README: the pige radar section said "all four portals". There are three.
  Every input field is now documented in a table instead of only the six that
  happened to appear in the example.

### 2026-08-29

- Fixed: a short run timeout (180 s or less) reserved the whole budget for
  the finishing phase, so the run stopped immediately and returned nothing.
  Scraping now always keeps at least half of whatever time the run has.

### 2026-08-09 (pre-push review fixes)

- **Fixed a request-stealing bug introduced by the concurrency change.** Every
  Crawlee crawler built without an explicit `requestQueue` resolves to the same
  process-cached default queue. Once the legs started running concurrently, a
  mixed input (one pap.fr URL + one seloger.com URL) could have the PAP
  CheerioCrawler dequeue SeLoger's browser request, fail it five times on a
  missing adapter and mark it handled — so SeLoger returned **zero** rows on the
  exact fallback path built to rescue it, and it broke symmetrically in the other
  direction. The browser fallback is now hoisted out of the portal legs and run
  exactly once, after every leg has settled, so only one Crawlee crawler is ever
  alive. (Named queues were rejected as the fix: they persist on the platform and
  would carry handled-request state into the next run.) `tests/queue.test.js`
  pins the invariant.
- **The browser fallback is now budgeted** — at most **20 %** of a run's rows,
  because the Chromium rung costs $1.214 per 1,000 against $0.1392 for the
  all-residential HTTP path. Both portals share one budget. When it trips the
  actor stops escalating, ships what it collected and says so in the run's status
  message. See `src/sources/browser-budget.js`.
- A single search URL that throws inside a portal leg no longer discards that
  portal's remaining URLs; it degrades to the browser rung like a walled URL.
- `maxMemoryMbytes` set to **2048**, matching the actor's current live default.
  Dropping to 1024 in the same push that triples concurrency and can start a
  Chromium crawler on the playwright image risks an OOM, which is a FAILED run.

### 2026-08-09 (later) — HTTP for all three portals, and a real fix for the timeouts

- **The 16.1 % timeout rate was structural, not random.** The three portals were
  crawled sequentially inside one shared run budget, so each leg's retry exposure
  compounded to roughly their sum — while the three standalone actors this bundle
  is built from measure 0 % / 0 % / 2.5 % each. Two changes fix the shape:
  - `src/budget.js` reserves the tail of the run for merge + PAP phone
    enrichment + push **before any crawling starts**. Rows already collected are
    now always delivered and billed; a slow portal costs coverage, never the run.
  - the three legs run **concurrently, each with its own deadline**. They are
    three different origins with three different proxy sessions, so overlapping
    them does not raise the request rate per origin — it turns the run's exposure
    from A+B+C into max(A,B,C).
- **SeLoger and Logic-Immo now scrape over plain HTTP** (impit + proxy ladder),
  inheriting the standalone actors' rewrites. Chromium survives only as their
  last fallback rung.
- **Drift is now a test failure.** `src/sources/*` are byte-identical copies of
  the standalone actors' modules, and `tests/sync.test.js` fails if any of them
  diverges. It already caught one pre-existing divergence (`pap.normalize.js`),
  which is tracked in that test's `KNOWN_DRIFT` list rather than left invisible.
- **Guardrails:** `maxMemoryMbytes: 1024` added, default run memory 2048 → 512
  (**not** 256 — `Collector.map` holds every raw record up to `1.5 × maxResults`,
  so the peak grows with run size and the 3×-peak rule does not apply cleanly).
  Dockerfile no longer passes `--omit=optional`.
- Projected cost per 1,000 listings: **$0.182**, against $1.283 measured on the
  browser path. Modelled from measured page weights — one platform run still has
  to confirm it.

### 2026-08-09

- Docs only: the Pricing section now states an illustrative all-in cost
  (list price + measured platform usage) instead of leaving platform usage
  unquantified, so the sticker price doesn't understate the real cost of a run.
  No price, input or code change.

### 2026-07-12

- Invalid input no longer fails the run. The run now finishes cleanly with a clear explanation in its status message, so a typo or an empty list is easy to spot and fix.
- List inputs now accept datasets piped straight from another scraper: entries
  that are objects are read through their usual key names (url/link,
  query/keyword/title, name/company and similar) instead of being rejected.

### 2026-07-11

- Billing: on paid runs every row is billed before it is written, and a run
  now stops visibly if billing fails instead of continuing unbilled.

### 2026-07-09

- Input is now tolerant of common shapes that previously failed the run: a bare
  string / bare object / delimited string for searchUrls is coerced into a list
  instead of erroring. Empty input still errors.

### 2026-07-06

- Simplified input: pagination depth is now handled automatically (technical field removed).

### 2026-07-05

- Published on Apify Store.

### 2026-07-04

- Initial release.
- Multi-source pige immobilière : SeLoger + PAP + Logic-Immo in one run, deduplicated across portals.
- Owner-direct (particulier) filter, advertiser phone numbers, €/m² and DPE.
- Monitor mode returns only NEW listings, with Slack/webhook alerts.
