# Changelog of Coches.net Scraper — Car Listings & Dealers (`rastriq/cochesnet-spain`) Actor

- **URL**: https://apify.com/rastriq/cochesnet-spain/changelog.md
- **Full Actor documentation**: https://apify.com/rastriq/cochesnet-spain.md

## Changelog

### 0.2 — 2026-09-16 · `stripSellerPII` — GDPR PII removal for private sellers

New boolean input `stripSellerPII` (default: `false`). When enabled, `phone` is set to `null`
for non-professional sellers. Coordinates are already municipal centroids and are not modified.
Professional seller data is never affected.

### 0.2 — 2026-08-23 · Stop at the last page, and say so when the slice cannot be finished

Coches.net reports `initialResults.totalPages`. We were not reading it, so a sweep discovered the
end of its slice by fetching one page too many — and on this source that extra request frequently
returns an anti-bot challenge. The run then reported `sweep_ended: "no_payload"` and
`reconcilable: false`, marking a sweep untrustworthy for delisting when it had in fact covered
everything it was allowed to reach.

Now the sweep stops on `page >= totalPages` with `sweep_ended: "last_page"`, which is one fewer
request and one less chance of being challenged.

#### The finding that matters more

`robots.txt` disallows `pg=7` onward, so we cap at 6 pages ≈ 210 listings. Measured while fixing
this:

| Slice | Listings | Pages | Fits in 6? |
|---|---:|---:|:--:|
| Soria, everything | 275 | 10 | no |
| Soria from €6,000 | 196 | 7 | no |
| Soria from €10,000 | 147 | 5 | **yes** |
| Segovia, everything | 614 | 21 | no |

**Soria is the smallest province in Spain and it does not fit.** Province granularity is therefore
never enough for delisting on coches.net: a sweep that cannot exhaust its slice cannot say a listing
is gone. A first-page warning now states this explicitly when `totalPages > maxPages`, naming the
listing count a slice has to stay under.

The canary was re-pointed accordingly (Soria from €10,000, 147 listings, 5 pages). Seeded and
repeated: `last_page`, `reconcilable: true`, 147 of 147 recognised.

### 0.2 — 2026-08-22 · `SEEN_IDS` carries its own trustworthiness

`SEEN_IDS` was a bare list of ids. A consumer reading `held − SEEN_IDS = sold` would have deleted
live vehicles whenever a sweep was truncated or blocked — and a rate-limited run, which scans
nothing, would have read as "every car in this area sold overnight".

The record now reports whether it can be trusted:

- **`reconcilable`** — true only when the sweep exhausted its slice AND scanned more than zero.
- **`sweep_ended`** — `feed_exhausted` / `last_page` / `max_pages` / `max_items` / `http_error` /
  `no_payload` / `unhandled_error`. Every termination path is now labelled at the point it happens.
- **`scope`** — the filters that define the slice this list speaks for. Absence is only meaningful
  inside it; comparing a provincial sweep against a national catalogue deletes the country.
- An unusable sweep logs a **warning** instead of passing silently.

`SUMMARY` carries `sweep_complete` and `sweep_ended` too, so the canary can track truncation.

Verified on the platform: a finished sweep reports `reconcilable: true, sweep_ended: "last_page"`;
a capped sweep reports `false` with the reason; and the promotion smoke run hit a real coches.net
rate limit and correctly reported `reconcilable: false, sweep_ended: "http_error"`.

### 0.2 — 2026-08-21

#### Fixed — P0: runs were silently truncating to a handful of listings

Since build **0.1.45 (31 July 2026, 21:20 UTC)** every run has been delivering roughly five
listings and then stopping, while reporting success.

`dataset_schema.json` declared `videos` as an array of strings. Coches.net returns **objects**:

```json
"videos": [{"url": "https://pro.video.fastly.ccdn.es/videos/…/master.m3u8",
            "autoPlay": true, "position": "FIRST", "provider": 2, "externalId": "…"}]
```

Apify validates every `push_data` against the dataset schema, so the first listing with a video
raised `InvalidRequestError: Schema validation failed`. That exception propagated out of the
scrape loop into a broad `except Exception`, which logged it and let the Actor exit 0. The
console showed a green run with output, which is why it went unnoticed for three weeks.

Measured on 137 live listings: **27 carry a video — 20%** — so a run died after ~5 listings.

Two changes:

- **`videos` is normalised to a list of URLs** in `_row()` and `_row_from_detail()`, which is
  what the schema documents and what consumers expect. Re-validated: 137/137 pass.
- **Pushes are batched with per-row fallback.** A batch failure degrades to row-by-row and an
  unpushable listing is dropped with a warning. One malformed listing can no longer abort a
  sweep — that was the real defect; the type mismatch was just what triggered it.

#### Added

- **`latitude`, `longitude`, `ine_municipality_code`, `geo_precision`.**
  Coches.net publishes no per-listing coordinates on any surface — search payload, detail page
  or Adevinta gateway. What it does expose is `location.cityId`, which is the official **INE
  municipality code** (verified 14/14 against the official register, present on 171/171 listings
  sampled). Coordinates are resolved offline from an embedded table of 8 217 municipality
  centroids (`georef-spain-municipio`, from official IGN/INE boundaries), covering 8 122 of the
  8 124 municipalities. No extra requests, no per-listing cost, no geocoding key.
  `geo_precision` is `"municipality"` (typically 1–8 km) or `"unavailable"`.
- **`postalCode` + `radiusKm` — radius search.** Coches.net filters on
  `locationLat`/`locationLong`/`locationDistance`; the visible text box is cosmetic. The site
  geocodes what the user types with the Google Places API in the browser; we resolve the postal
  code from an embedded GeoNames table instead. Note the radius is exact for private sellers,
  while dealers with national or multi-province stock appear in any radius search regardless of
  distance — that is coches.net's own behaviour, documented rather than hidden.
- **`province`** — restrict to one province by INE code. Useful for sharding a large extraction
  into queries that stay within the page limit.
- **`cursorId`** — namespaces the incremental ledger.
- **`emitSeenIds`** — writes every observed listing id to `SEEN_IDS` for delisting detection.
- **Geo and Delta views** in the dataset schema.
- **Offline test suite** (`tests/test_v02.py`, 38 assertions) including schema conformance
  against 137 live listings.

#### Changed

- **Incremental mode rebuilt on a ledger.** v0.1 stored `max(id)` as a watermark. Coches.net ids
  are sequential and monotonic with creation date (verified 170/170), so the primitive looked
  sound — but the search feed is ordered by `relevance`, which coches.net shuffles between
  sessions. With a shuffled feed the watermark advances past ids the run never saw, and since it
  only moves forward those listings fall below it permanently. Same class of loss as the Wallapop
  timestamp cursor, reached by a different route.

  The ledger assumes nothing about ordering: a listing is emitted when its id is unknown, or when
  a fingerprint over `price / km / title / reservation / price drop / seller / offer type` moved.
  Emitted rows carry `_delta_status`, `_changed_fields`, `_previous_price` and `_price_delta`.

- **Cursor state is namespaced per stream.** v0.1 used one global key, so a search for one make
  would cause a later search for another to skip everything below its watermark.

- **`maxPages` is hard-capped at 6.** `robots.txt` disallows `pg=7` through `pg=69` and this
  Actor respects it. The previous default of `20` when the field was absent violated it on any
  API call that omitted `maxPages`. Build a large catalogue by sharding the query space
  (province × make × price band), not by paginating deeper. A capped request is logged.

- **`SUMMARY` is now diagnostic**: `scanned`, `emitted_new`, `emitted_updated`,
  `unchanged_skipped`, `ledger_size`, `max_pages_used`, `engine`. A delta run that scans listings
  and emits none is a correct outcome and logged as such; a run that scans nothing raises.

#### Migration

Automatic. v0.2 uses a new store (`cochesnet-delta-state`) and does not carry over the v0.1 id
watermark, which could not distinguish "already delivered" from "never seen". The first run with
`enableCursor: true` captures a full baseline and seeds the ledger.

Anyone who ingested between 31 July and this release should treat their coches.net data as
incomplete and re-run the baseline: the truncation dropped listings that cannot be recovered
retrospectively.

#### Compatibility

Backward compatible. Existing inputs keep their names and semantics; new fields are optional.
`maxPages` values above 6 are capped rather than rejected.
