Paramètres d'affichage

Choisissez un thème pour personnaliser l'apparence du site.

Tech

Qu’est-ce que l’algorithme de rapprochement de bases bâtimentaires du RNB ?

Publié le 25 avril 2024

Le RNB dispose de plusieurs algorithmes de rapprochement de bases bâtimentaires. Objectif : Séquencer de différentes façons, au regard des données fournies. Retrouvez la méthodologie utilisée avec un exemple concret, puis consultez le code source de l’outil de rapprochement du RNB

💡
En mars 2024, le RNB a collaboré avec les équipes en charge de la base de données des équipements sportifs et des lieux de pratiques, Data ES, du Ministère des Sports et des Jeux Olympiques et Paralympiques. L’objectif de cette collaboration portait sur la mise en oeuvre du service de rapprochement de bases bâtimentaires du RNB, à savoir l’intégration des ID Bâtiments du RNB au sein de leur base de données.

Plus d'information : Consultez le code source de l’outil de rapprochement du RNB sur le dépôt public .

Caractéristiques de la base Data ES et Présentation des résultats du rapprochement

Lors du rapprochement, la base de données Data ES contenait 86 082 lignes correspondant à des bâtiments sportifs répartis sur l’ensemble du territoire français. Sur les différents attributs associés à un bâtiment, 3 informations clefs ont permis d’obtenir les ID Bâtiments du RNB :

  • Latitude et longitude du bâtiment (placés à la main par les enquêteurs de Data ES)
  • Nom du bâtiment
  • Adresse du bâtiment

Grâce aux outils de rapprochement du référentiel et avec la précision des informations récoltées par les équipes de Data ES, l’équipe du RNB a pu attacher 62 868 identifiants du RNB (soit 73 % du fichier) à la base de données des équipements sportifs.

La suite du rapprochement de Data ES se fera au fil de l’eau, grâce à l’intégration d’un sélecteur de bâtiment du RNB, au sein de leur outil de saisie. Cela permettra à leurs utilisateurs d’identifier et de sélectionner un bâtiment directement sur la carte du RNB.

Méthodologie de rapprochement des bases bâtimentaires

Le RNB dispose de plusieurs algorithmes de rapprochement de bases bâtimentaires. Objectif : Séquencer de différentes façons, au regard des données fournies.

Pour Data ES, l’équipe du référentiel a utilisé 3 outils de rapprochement :

  1. Rapprochement par proximité au point fourni : cet outil a permis de fournir l’essentiel des résultats
  2. Rapprochement par géocodage de l’adresse du bâtiment
  3. Rapprochement par géocodage du nom du bâtiment

Pour chaque ligne fournie, ces outils passent dans l’ordre, les uns après les autres jusqu’à ce que l’un d’entre eux trouve un résultat.

Le code source de l’outil de rapprochement du RNB est consultable sur le dépôt public de votre géocommun.

C’est quoi le rapprochement par proximité au point fourni ?

Pour chaque point fourni, vous trouverez l’approche réalisée :

  1. Récupérer les 2 bâtiments RNB les plus proches de ce point dans un rayon de 30 mètres, triés par distance croissante, accompagnés de la distance entre le bâtiment et le point
  2. si le bâtiment plus proche est à zéro mètre du point (autrement dit, si le point est placé sur le bâtiment) : on renvoie l'identifiant du bâtiment
  3. si le bâtiment le plus proche est à moins de 8 mètres
    1. si il y a un seul bâtiment dans un rayon de 30 mètres : on retourne l'identifiant du bâtiment
    2. sinon, si le second bâtiment le plus proche est assez loin du premier bâtiment : on renvoie l'identifiant du bâtiment
  4. on renvoie un résultat vide, cette première approche a échoué

💡 Cette approche a le double avantage d’être précise (les points fournis par les enquêteurs de Data ES sont particulièrement bien placés) et d’être très performante. En effet, elle fait appel qu’à la base de données du RNB et ne dépend pas de services externes.

Voici la class ClosestFromPointHandler en charge de cette approche :


class ClosestFromPointHandler(AbstractHandler):
    _name = "closest_from_point"

    def __init__(self, closest_radius=30, isolated_bdg_max_distance=8):
        self.closest_radius = closest_radius
        self.isolated_bdg_max_distance = isolated_bdg_max_distance

    def _guess_batch(self, guesses: dict) -> dict:
        tasks = []

        with concurrent.futures.ThreadPoolExecutor() as executor:
            for guess in guesses.values():
                future = executor.submit(self._guess_one, guess)
                future.add_done_callback(lambda future: connections.close_all())
                tasks.append(future)

            for future in concurrent.futures.as_completed(tasks):
                guess = future.result()
                guesses[guess["input"]["ext_id"]] = guess

        return guesses

    def _guess_one(self, guess: dict) -> dict:
        lat = guess["input"].get("lat", None)
        lng = guess["input"].get("lng", None)

        if not lat or not lng:
            return guess

        # Get the two closest buildings
        closest_bdgs = get_closest(lat, lng, self.closest_radius)[:2]

        if not closest_bdgs:
            return guess

        first_bdg = closest_bdgs[0]
        # Is the the point is in the first building ?
        if first_bdg.distance.m <= 0:
            guess["match"] = first_bdg
            guess["match_reason"] = "point_on_bdg"
            return guess

        # Is the first building within 8 meters and the second building is far enough ?
        if first_bdg.distance.m <= self.isolated_bdg_max_distance:
            if len(closest_bdgs) == 1:
                # There is only one building close enough. No need to compare to the second one.
                guess["match"] = first_bdg
                guess["match_reason"] = "isolated_closest_bdg"
                return guess

            if len(closest_bdgs) > 1:
                # There is at least one other close building. We compare the two closest buildings distance to the point.
                second_bdg = closest_bdgs[1]
                min_second_bdg_distance = self._min_second_bdg_distance(
                    first_bdg.distance.m
                )
                if second_bdg.distance.m >= min_second_bdg_distance:
                    guess["match"] = first_bdg
                    guess["match_reason"] = "isolated_closest_bdg"
                    return guess

        # We did not find anything. We return guess as it was sent.
        return guess

    @staticmethod
    def _min_second_bdg_distance(first_bdg_distance: float) -> float:
        # The second building must be at least 10 meters away from the point
        min_distance_floor = 10.0

        # The second building must be at least 3 times the distance of the first building
        ratio = 3
        min_distance_w_ratio = first_bdg_distance * ratio

        return max(min_distance_floor, min_distance_w_ratio)
        
        
def get_closest(lat, lng, radius):
    qs = (
        Building.objects.all()
        .filter(is_active=True)
        .filter(status__in=BuildingStatus.REAL_BUILDINGS_STATUS)
    )

    point_geom = Point(lng, lat, srid=4326)

    qs = (
        qs.extra(
            where=[
                f"ST_DWITHIN(shape::geography, ST_MakePoint({lng}, {lat})::geography, {radius})"
            ]
        )
        .annotate(distance=Distance("shape", point_geom))
        .order_by("distance")
    )

    return qs

En quoi consiste le rapprochement par géocodage de l’adresse du bâtiment ?

L’utilisation des adresses est un sujet plus difficile que l’utilisation d’un point. En effet, le lien bâtiment ↔ adresse est un classique des problématiques des géomaticiens. Il n’existe à ce jour aucune base de haute qualité faisant ce lien.

A ce sujet, il faut rendre hommage au travail réalisé depuis des années par la BAN et les BAL pour constituer le référentiel des adresses, ainsi qu’au CSTB pour son travail récent sur l’association bâtiment ↔ adresse au sein de la BDNB.

Vous trouverez la logique utilisée pour le rapprochement par adresse :

  1. Récupérer l’identifiant BAN en géocodant l’adresse fournie
  2. Récupérer tous les bâtiments RNB les plus proches du point fourni dans un rayon de 100 mètres et rattachés à l’identifiant BAN obtenu, triés par distance croissante, accompagnés de la distance entre le bâtiment et le point
  3. Si, le nombre de résultats est strictement égal à 1 : on renvoie l'identifiant du bâtiment
  4. on renvoie un résultat vide, l’approche par géocodage de l’adresse a échoué

💡 La vérification dans un large rayon (100 mètres autour du point) et la stricte limitation à un seul résultat attendu permet en partie de parer à la confiance limitée que l’équipe du référentiel a pour le moment, au regard du lien bâtiment ↔ adresse.

Un sujet sur lequel le RNB souhaite apporter ses contributions et pour lequel, l’équipe envisage plusieurs pistes, à savoir :

  • Exploiter les points de types bâtiments disponibles dans les BAL et absents de la BAN
  • Favoriser la présence des bâtiments au sein de BAL
  • Accueillir les contributions citoyennes et administratives pour corriger ces liens

Voici la class GeocodeAddressHandler en charge de cette approche :


class GeocodeAddressHandler(AbstractHandler):
    _name = "geocode_address"

    def __init__(self, sleep_time=0.8, closest_radius=100):
        self.sleep_time = sleep_time
        self.closest_radius = closest_radius

    def _guess_batch(self, guesses: dict) -> dict:
        for guess in guesses.values():
            guess = self._guess_one(guess)
            guesses[guess["input"]["ext_id"]] = guess

        return guesses

    def _guess_one(self, guess: dict) -> dict:
        lat = guess["input"].get("lat", None)
        lng = guess["input"].get("lng", None)
        address = guess["input"].get("address", None)

        if not address or not lat or not lng:
            return guess

        # We sleep a little bit to avoid being throttled by the geocoder
        time.sleep(self.sleep_time)

        ban_id = self._address_to_ban_id(address, lat, lng)

        if ban_id:
            close_bdg_w_ban_id = get_closest(lat, lng, self.closest_radius).filter(
                addresses__id=ban_id
            )

            if close_bdg_w_ban_id.count() == 1:
                guess["match"] = close_bdg_w_ban_id.first()
                guess["match_reason"] = "precise_address_match"

        return guess

    @staticmethod
    def _address_to_ban_id(address: str, lat: float, lng: float) -> Optional[str]:
        geocoder = BanGeocoder()
        geocode_response = geocoder.geocode(
            {
                "q": address,
                "lat": lat,
                "lon": lng,
                "type": "housenumber",
            }
        )

        if geocode_response.status_code != 200:
            return

        geo_results = geocode_response.json()

        if "features" in geo_results and geo_results["features"]:
            best = geo_results["features"][0]

            if best["properties"]["score"] >= 0.8:
                return best["properties"]["id"]

Qu’est-ce que le rapprochement par géocodage du nom du bâtiment ?

Si l’approche par point et l’approche par adresse ont échoué, l’équipe du RNB procède à la mis en oeuvre de la dernière étape : l’utilisation du nom du bâtiment.

Cette approche n’est pas toujours disponible.

Cependant, dans le cas des bâtiment sportifs, il est courant de trouver des “Centre nautique Laure Manaudou” ou des “Gymnase Nelson Paillou” qui constituent des bons points d’accroche.

Le RNB ne contient pas les noms des bâtiments. Cependant, grâce au travail des contributeurs d’ OpenStreetMap et avec un outil de géocodage comme Photon, il est possible d’obtenir des résultats.

Vous trouverez l’approche réalisée :

  1. Obtention d’un bâtiment OSM et de son point à partir d’un géocodage Photon du nom du bâtiment
  2. Si Photon trouve un résultat :
    1. On récupère le bâtiment RNB contenant le point du bâtiment OSM
    2. Si un bâtiment correspond :
      1. on renvoie l'identifiant du bâtiment
  3. on renvoie un résultat vide, l’approche par géocodage du nom a échoué

💡 Il est assez courant de recevoir des bases à rapprocher contenant des noms de bâtiment. Cette approche permet d’obtenir quelques pourcentages de correspondances complémentaires, en bonus des approches précédentes. L’équipe du Référentiel aimerait, à l’occasion, améliorer cette approche. Le premier axe imaginé serait de contribuer au dépôt Photon, notamment pour améliorer sa prise en compte du français.

Voici la class GeocodeNameHandler en charge de cette approche :


class GeocodeNameHandler(AbstractHandler):
    _name = "geocode_name"

    def __init__(self, sleep_time=0.8):
        self.sleep_time = sleep_time

    def _guess_batch(self, guesses: dict) -> dict:
        for guess in guesses.values():
            guess = self._guess_one(guess)
            guesses[guess["input"]["ext_id"]] = guess

        return guesses

    def _guess_one(self, guess: dict) -> dict:
        lat = guess["input"].get("lat", None)
        lng = guess["input"].get("lng", None)
        name = guess["input"].get("name", None)

        if not lat or not lng or not name:
            return guess

        # We sleep a little bit to avoid being throttled by the geocoder
        time.sleep(self.sleep_time)

        osm_bdg_point = self._geocode_name_and_point(name, lat, lng)

        if osm_bdg_point:
            # todo : on devrait filtrer pour n'avoir que les bâtiments qui ont un statut de bâtiment réel
            bdg = Building.objects.filter(shape__contains=osm_bdg_point).first()

            if isinstance(bdg, Building):
                guess["match"] = bdg
                guess["match_reason"] = "found_name_in_osm"
                return guess

        return guess

    @staticmethod
    def _geocode_name_and_point(name: str, lat: float, lng: float) -> Optional[Point]:
        geocode_params = {
            "q": name,
            "lat": lat,
            "lon": lng,
            "lang": "fr",
            "limit": 1,
        }

        geocoder = PhotonGeocoder()

        response = geocoder.geocode(geocode_params)

        geo_result = response.json()

        if geo_result.get("features", None) and geo_result["features"][0]["properties"][
            "type"
        ] in [
            "building",
            "house",
            "construction",
        ]:
            lat = geo_result["features"][0]["geometry"]["coordinates"][1]
            lng = geo_result["features"][0]["geometry"]["coordinates"][0]
            return Point(lng, lat, srid=4326)
        else:
            return

🎮
Vous aussi, intégrer les ID bâtiments du RNB et stocker ses identifiants, au sein de votre base.

Rien de plus simple :

Consultez la documentation API accessible gratuitement pour intégrer le RNB à vos systèmes

Écrivez-nous à l’adresse suivante pour toutes demandes de précisions techniques sur rnb@beta.gouv.fr

Infolettre et réseaux
Restez informé des actualités du RNB en vous inscrivant à l'infolettre ou en nous suivant sur LinkedIn.