# Arquitectura de Scrapers

## Jerarquía de clases

```
BaseScraper (abstracta)
│   baseUrl, sourceName
│   fetchHtml()         — HTTP con axios, reintentos, User-Agent rotativo
│   parsePrice()        — extrae número de strings como "150.000 €"
│   sanitizeText()      — limpia espacios, entidades HTML
│   validateResults()   — filtra propiedades sin precio o URL
│   normalizeType()     — usa normalizePropertyType() de utils/normalizeType
│   detectMunicipality()— busca zona activa en texto de ubicación
│   getActiveZones()    — consulta DB con caché de 60 segundos
│
├── Scrapers Cheerio (HTTP directo)
│   ├── HabitacliaScraper
│   ├── IdealistaScraper        (cheerio, respaldo del Playwright)
│   ├── FotocasaScraper         (cheerio)
│   ├── PisosScraper
│   ├── TrovatScraper
│   ├── KyeroScraper
│   ├── ThinkSpainScraper
│   ├── MilanunciosScraper
│   ├── YaencontreScraper
│   ├── CasasapoScraper
│   ├── VibboScraper
│   ├── GlobalizaScraper
│   ├── NuroaScraper
│   ├── EngelVolkersScraper
│   ├── TecnocasaScraper
│   ├── EraSpainScraper
│   ├── RemaxScraper
│   ├── InmobiliariaScraper
│   ├── SpainHousesScraper
│   └── HolpropScraper
│
└── PlaywrightBaseScraper (extiende BaseScraper)
        fetchHtmlPlaywright()   — Chromium headless, stealth, cookie handler
        pageTimeoutMs          — timeout configurable
        waitForSelector        — selector CSS para esperar JS
        │
        ├── IdealistaPlaywrightScraper   ← usado por defecto para Idealista
        └── FotocasaPlaywrightScraper    ← usado por defecto para Fotocasa
```

> **Nota:** El `SCRAPER_MAP` en `propertyService.ts` mapeea `IdealistaScraper` → `IdealistaPlaywrightScraper` automáticamente. Si Playwright falla, basta cambiar la clave para usar el scraper Cheerio.

---

## Flujo de un Scraper

```
scrape()
  │
  ├─ getActiveZones()           → zonas activas de la BD (caché 60s)
  │
  ├─ for each zone:
  │    buildUrl(zone.slug)       → URL del portal para ese municipio
  │    fetchHtml(url)            → HTML con axios (o fetchHtmlPlaywright)
  │    cheerio.load(html)
  │    $("selector").each()
  │      │
  │      ├─ sanitizeText(title)
  │      ├─ parsePrice(priceRaw)
  │      ├─ normalizeType(title) → tipo canónico
  │      ├─ detectMunicipality() → nombre de zona coincidente
  │      └─ push to properties[]
  │
  └─ validateResults(properties) → filtra precio=0/url vacía
```

---

## Normalización de Tipos de Propiedad

Archivo: `src/utils/normalizeType.ts`

Convierte términos específicos de cada portal al tipo canónico:

| Términos de portal                               | Tipo canónico |
| ------------------------------------------------ | ------------- |
| piso, apartment, flat, planta, estudio           | `piso`        |
| ático, atico, penthouse, azotea                  | `ático`       |
| chalet, villa, casa unifamiliar, house, detached | `chalet`      |
| terreno, parcela, solar, plot, land              | `terreno`     |
| local, shop, premises, comercial                 | `local`       |
| garaje, parking, garage                          | `garaje`      |
| oficina, office, despacho                        | `oficina`     |
| trastero, storage, almacén                       | `trastero`    |
| finca, cortijo, alquería, country house          | `finca`       |
| *(sin coincidencia)*                             | `casa`        |

---

## Scrapers Playwright (Anti-bot)

`PlaywrightBaseScraper.fetchHtmlPlaywright()` aplica:

- **Chromium headless** con flags anti-detección
- **Viewport aleatorio** de una lista de 4 resoluciones comunes
- **Locale`es-ES`** para recibir contenido localizado
- **Headers** que imitan navegador real (User-Agent, Accept-Language, sec-ch-ua)
- **Cierre automático de modales** de cookies (busca botones: "Acepto", "Aceptar", "Accept")
- **Timeout configurable** por scraper (`pageTimeoutMs`, default 30s)
- **`waitForSelector`** para esperar que el JS renderice el listado de propiedades

---

## Estrategia Por Portal

| Portal          | Tecnología | Notas                                  |
| --------------- | ---------- | -------------------------------------- |
| Idealista       | Playwright | Muy anti-bot; Playwright obligatorio   |
| Fotocasa        | Playwright | React SPA; contenido dinámico          |
| Habitaclia      | Cheerio    | SSR, HTML estático                     |
| Pisos.com       | Cheerio    | Usa pisos_slug de la tabla zones       |
| Trovit          | Cheerio    | Meta-buscador, query por `?what=`      |
| Kyero           | Cheerio    | Portal UK para compradores extranjeros |
| ThinkSpain      | Cheerio    | Portal para extranjeros                |
| Milanuncios     | Cheerio    | Clasificados; anti-bot moderado        |
| Yaencontre      | Cheerio    | Portal nacional                        |
| Casasapo        | Cheerio    | Portal nacional                        |
| Vibbo           | Cheerio    | Clasificados Schibsted                 |
| Globaliza       | Cheerio    | Fuerte presencia en Valencia           |
| Nuroa           | Cheerio    | Meta-buscador internacional            |
| Engel & Völkers | Cheerio    | Inmobiliaria premium                   |
| Tecnocasa       | Cheerio    | Franquicia inmobiliaria                |
| ERA Spain       | Cheerio    | Franquicia ERA                         |
| REMAX           | Cheerio    | Franquicia global                      |
| Inmobiliaria.es | Cheerio    | Directorio nacional                    |
| SpainHouses     | Cheerio    | Para compradores internacionales       |
| Holprop         | Cheerio    | Portal UK/Internacional                |
