> ## Documentation Index
> Fetch the complete documentation index at: https://doc.fluximmo.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Recherche analytique PROPERTIES (Paris achat appartement)

> Cartographier le marché Paris achat appartement avec déduplication multi-portails, pagination cursor, stats prix m².

## Goal

Construire un dataset **dédupliqué** d'appartements à l'achat sur Paris (puis l'Île-de-France), paginer pour l'exhaustivité, et calculer des statistiques **prix au m²** par département — sans biais lié aux doublons inter-portails.

## Scénario

Vous êtes une fintech ou une équipe analytics qui doit cartographier le marché parisien : stock disponible, médianes prix/m², distribution par dpt. Compter les **annonces** brutes (Adverts) gonfle le volume car chaque bien est publié sur 3-8 portails. Vous travaillez donc sur **Properties** (entités dédupliquées) avec pagination cursor et un workflow reproductible.

<Snippet file="/snippets/banniere-llm-context.mdx" />

## Étapes

<Steps>
  <Step title="1. Choisir Properties (et pas Adverts)">
    Une `Property` = 1 bien physique unique = N adverts agrégés. C'est l'objet à utiliser pour tout calcul agrégé : compter des biens (pas des publications), médianes prix, stocks, durées en ligne.

    <Snippet file="/snippets/decision-matrix-property-vs-advert.mdx" />

    Détail de l'asymétrie et des anti-patterns : [/concepts/property-vs-advert](/concepts/property-vs-advert).
  </Step>

  <Step title="2. Construire le payload — combo standard prod">
    Trois bonnes pratiques :

    * `meta.isTotallyOffline: false` → exclut les biens dont **toutes** les adverts sont offline (sinon votre stock contient des fantômes).
    * `sortBy: "FIRST_SEEN_AT"` + `orderBy: "DESC"` → vous parcourez du plus récent au plus ancien, idéal pour reprendre une exploration à chaud.
    * `meta.firstSeenAt.min` → borne temporelle pour ignorer le très ancien (typiquement `2025-01-01T00:00:00.000Z`).
    * `size: 100` → max autorisé sur l'endpoint full search (lite est plafonné à 25).

    ```bash theme={null}
    curl -X POST "https://api.fluximmo.io/v2/protected/properties/search" \
      -H "x-api-key: $FLUXIMMO_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "size": 100,
        "sortBy": "FIRST_SEEN_AT",
        "orderBy": "DESC",
        "search": {
          "filterProperty": {
            "location": [{ "postalCode": "75001" }],
            "type": ["CLASS_FLAT", "CLASS_HOUSE", "CLASS_PROGRAM"],
            "offer": [{ "type": "OFFER_BUY" }],
            "price": { "initial": { "value": { "min": 100000, "max": 350000 } } },
            "habitation": {
              "surface":     { "total": { "min": 30, "max": 110 } },
              "bedroomCount":{ "min": 1, "max": 3 }
            },
            "meta": {
              "isTotallyOffline": false,
              "firstSeenAt": { "min": "2025-01-01T00:00:00.000Z" }
            }
          }
        }
      }'
    ```
  </Step>

  <Step title="3. Lire le `searchAfterHash` retourné">
    La réponse est wrappée sous `data` et contient `items[]` (jusqu'à 100 properties), `count`, et un champ `searchAfterHash`. C'est un **cursor** opaque : ne le décodez pas, contentez-vous de le replacer dans la requête suivante.

    ```json theme={null}
    {
      "data": {
        "items": [ /* up to 100 Property */ ],
        "count": 1247,
        "searchAfterHash": "eyJzZWFyY2hfYWZ0ZXIiOlsxNzM..."
      }
    }
    ```
  </Step>

  <Step title="4. Paginer pour exhaustivité (boucle cursor)">
    Boucle : tant que la dernière page ramène `size` items, replongez avec `searchAfterHash`. Stoppez quand `len(items) < size` (fin du dataset) ou quand votre cap user-defined est atteint.

    ```text theme={null}
    # Pseudocode pagination
    all_items = []
    cursor = null
    HARD_CAP = 5000

    while all_items.length < HARD_CAP:
        payload = base_payload
        if cursor: payload.searchAfterHash = cursor

        resp = POST /v2/protected/properties/search (payload)
        items = resp.data.items
        all_items.append_all(items)

        if items.length < SIZE: break        # dernière page
        cursor = resp.data.searchAfterHash
        if !cursor: break
    ```

    Deuxième page en curl direct :

    ```bash theme={null}
    curl -X POST "https://api.fluximmo.io/v2/protected/properties/search" \
      -H "x-api-key: $FLUXIMMO_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "size": 100,
        "sortBy": "FIRST_SEEN_AT",
        "orderBy": "DESC",
        "searchAfterHash": "<cursor de la page 1>",
        "search": { "filterProperty": { "location": [{"postalCode":"75001"}], "type": ["CLASS_FLAT"], "offer": [{"type":"OFFER_BUY"}], "meta": {"isTotallyOffline": false} } }
      }'
    ```
  </Step>

  <Step title="5. Calculer le prix au m² par département">
    Une fois le dataset collecté, agrégez par les 2 premiers caractères du `postalCode` (= département).

    ```text theme={null}
    # Pseudocode agrégation
    by_dpt = {}
    foreach p in all_items:
        cp        = p.location.postalCode
        price_psm = p.price.latest.valuePerArea
        if cp and price_psm:
            dpt = cp[0..2]
            by_dpt[dpt].append(price_psm)

    foreach dpt, vals in by_dpt:
        sort(vals)
        n   = vals.length
        q1  = vals[floor(0.25 * (n - 1))]
        med = vals[floor(0.50 * (n - 1))]
        q3  = vals[floor(0.75 * (n - 1))]
        print(dpt, n, q1, med, q3)
    ```
  </Step>

  <Step title="6. Étendre à toute l'Île-de-France (multi-zones OR)">
    Pour passer de Paris-1er à Paris + petite couronne, remplacez `location[]` par 4 zones combinées en `OR`.

    ```json theme={null}
    {
      "size": 100,
      "sortBy": "FIRST_SEEN_AT",
      "orderBy": "DESC",
      "search": {
        "filterProperty": {
          "location": [
            { "department": "75" },
            { "department": "92" },
            { "department": "93" },
            { "department": "94" }
          ],
          "type": ["CLASS_FLAT"],
          "offer": [{ "type": "OFFER_BUY" }],
          "meta": {
            "isTotallyOffline": false,
            "firstSeenAt": { "min": "2025-01-01T00:00:00.000Z" }
          }
        }
      }
    }
    ```

    Logique multi-zones complète et autres modes (bbox, geoDistance) : [/concepts/recherche-geographique](/concepts/recherche-geographique).
  </Step>
</Steps>

## Architecture / flow

```mermaid theme={null}
flowchart LR
  C["Votre script<br/>(POST search)"] --> FX["Fluximmo<br/>(dedup engine)"]
  FX --> R1["Page 1<br/>+ searchAfterHash"]
  R1 --> C
  C --> FX2["POST avec<br/>searchAfterHash"]
  FX2 --> R2["Page 2..N"]
  R2 --> DF["DataFrame<br/>(en mémoire)"]
  DF --> AGG["groupby(dpt)<br/>+ describe"]
  AGG --> OUT["Stats prix m²<br/>par dpt"]
```

## Pièges fréquents

<Warning>
  * **Confondre Properties et Adverts** → analytics biaisé : un même bien compté 3-8 fois (1 par portail) gonfle le stock et fausse les médianes. Toujours `Properties` pour les KPIs marché.
  * **Oublier `meta.isTotallyOffline: false`** → vous comptez des biens dont toutes les annonces sont offline (fantômes du catalogue).
  * **Tenter une pagination par offset** : ça n'existe pas. Le seul mode est cursor `searchAfterHash`. Ne décodez pas le cursor — c'est opaque.
  * **Mixer `postalCode` et `city` dans la même entrée** : `city` est ignoré par le moteur. Voir [warning city deprecated](/concepts/recherche-geographique).
  * **Dépasser `size` max** : 100 sur full search, 25 sur lite search. Au-delà, l'API reject ou tronque.
  * **Ne pas dédoublonner sur `flxId`** entre pages : un append concurrent peut renvoyer un même item deux fois si le tri change. Conservez un `set` des `flxId` vus.
</Warning>

## Pour aller plus loin

* [Property vs Advert](/concepts/property-vs-advert)
* [Filtres communs](/concepts/filtres-communs)
* [Recherche géographique](/concepts/recherche-geographique)
* [Search Properties (référence API)](/api-v2-reference/properties-search/search-properties)
* [Lite Search Properties (référence API)](/api-v2-reference/properties-search/lite-search-properties)
* [Playbook — recherche géographique avancée](/playbooks/recherche-geographique-avancee)

<Snippet file="/snippets/cta-cle-test-gratuite.mdx" />
