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 :
- Rapprochement par proximité au point fourni : cet outil a permis de fournir l’essentiel des résultats
- Rapprochement par géocodage de l’adresse du bâtiment
- 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 :
- 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
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âtimentsi
le bâtiment le plus proche est à moins de 8 mètressi
il y a un seul bâtiment dans un rayon de 30 mètres :↳
on retourne l'identifiant du bâtimentsinon, si
le second bâtiment le plus proche est assez loin du premier bâtiment :↳
on renvoie l'identifiant du bâtiment
↳
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 :
- Récupérer l’identifiant BAN en géocodant l’adresse fournie
- 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
Si
, le nombre de résultats est strictement égal à 1 :↳
on renvoie l'identifiant du bâtiment↳
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 :
- Obtention d’un bâtiment OSM et de son point à partir d’un géocodage Photon du nom du bâtiment
Si
Photon trouve un résultat :- On récupère le bâtiment RNB contenant le point du bâtiment OSM
- Si un bâtiment correspond :
↳
on renvoie l'identifiant du bâtiment
↳
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
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