1uyxb2hqetsj3wjazvxpvyw.jpeg

Las relaciones se utilizan en OSM para agrupar muchos otros elementos en un solo objeto. Aquí nos centraremos únicamente en los (multi) polígonos. Estos elementos específicos tienen un type etiqueta con uno de dos valores: boundaryy multipolygon.

Este tipo de objeto es el más complejo para reconstruir la geometría y aquí está la lista de pasos que debemos seguir:

  • Seleccionar relaciones con las adecuadas type valor.
  • Desanide todas las referencias relacionadas con la relación y mantenga solo las referencias de vía; solo necesitamos referencias de vía relacionadas para reconstruir un polígono.
  • Seleccione las vías requeridas con geometrías de cadena lineal; aquí podemos utilizar los pasos de la construcción de vías.
  • Asigne un rol ‘externo’ a la forma ref si es null y verifique si alguna referencia de la relación tiene el rol «externo»; si una relación no tiene referencias «externas», trátelas a todas como «externas».
  • Agrupe todas las cadenas de líneas por función ‘externa’ e ‘interna’ y combínelas en una sola cadena múltiple; muchas relaciones se definen con múltiples cadenas de líneas únicas que solo juntas crean un polígono cerrado.
  • Divida cadenas múltiples en cadenas lineales individuales de bucle cerrado y guárdelas como polígonos.
  • Divida las geometrías en polígonos «externos» e «interiores». Estos se pueden extraer de la ref_role columna del objeto de relación.
  • Para cada polígono ‘exterior’, seleccione todos los polígonos ‘interiores’ que estén completamente dentro de él y haga agujeros en este polígono.
  • Haz una unión de todos los polígonos ‘externos’ con agujeros.

Comencemos seleccionando relaciones con valores de etiqueta coincidentes.

CREATE TEMP TABLE matching_relations AS 
SELECT id, tags
FROM ST_READOSM('monaco-latest.osm.pbf')
WHERE kind = 'relation' AND len(refs) > 0
AND tags IS NOT NULL AND cardinality(tags) > 0
AND list_contains(map_keys(tags), 'type')
AND list_has_any(map_extract(tags, 'type'), ['boundary', 'multipolygon']);

SELECT * FROM matching_relations;

┌──────────┬───────────────────────────────────────────────────────────────────┐
│ id │ tags │
│ int64 │ map(varchar, varchar) │
├──────────┼───────────────────────────────────────────────────────────────────┤
│ 7385 │ {ISO3166-2=FR-06, admin_level=6, alt_name:de=Meeralpen, border_… │
│ 8654 │ {ISO3166-2=FR-PAC, admin_level=4, border_type=region, boundary=… │
│ 36990 │ {ISO3166-1=MC, ISO3166-1:alpha2=MC, ISO3166-1:alpha3=MCO, admin… │
│ 174558 │ {admin_level=8, boundary=administrative, name=Roquebrune-Cap-Ma… │
│ 174562 │ {admin_level=8, boundary=administrative, name=Beausoleil, name:… │
│ 174956 │ {admin_level=8, alt_name:it=Capo d`Aglio, boundary=administrati… │
│ 174958 │ {admin_level=8, boundary=administrative, name=La Turbie, name:i… │
│ 393226 │ {building=castle, castle_type=palace, charge=10€, email=visites… │
│ 393481 │ {building=school, name=Pensionnat des Dames de Saint-Maur, type… │
│ 1124039 │ {ISO3166-1=MC, ISO3166-1:alpha2=MC, ISO3166-1:alpha3=MCO, ISO31… │
│ · │ · │
│ · │ · │
│ · │ · │
│ 14399505 │ {area=yes, floating=yes, man_made=pier, type=multipolygon} │
│ 16248281 │ {building=apartments, name=F, type=multipolygon} │
│ 16248282 │ {building=apartments, name=E, type=multipolygon} │
│ 16248283 │ {building=apartments, name=D, type=multipolygon} │
│ 16248284 │ {building=apartments, name=C, type=multipolygon} │
│ 16248285 │ {building=apartments, name=B, type=multipolygon} │
│ 16248286 │ {building=apartments, name=A, type=multipolygon} │
│ 16250182 │ {addr:country=MC, alt_name=Parc de la Roseraie, leisure=park, n… │
│ 16261416 │ {natural=water, type=multipolygon, water=pond} │
│ 16467322 │ {admin_level=2, boundary=land_area, land_area=administrative, n… │
├──────────┴───────────────────────────────────────────────────────────────────┤
│ 78 rows (20 shown) 2 columns │
└──────────────────────────────────────────────────────────────────────────────┘

Ahora desanidaremos las listas de referencias y las dividiremos en filas individuales. Al igual que con las formas, aquí también utilizaremos las funciones de indexación de DuckDB para recordar el orden de los elementos en la lista original. Además, sólo conservaremos el way árbitros.

CREATE TEMP TABLE matching_relations_with_ways_refs AS
WITH unnested_relation_refs AS (
SELECT
r.id,
UNNEST(refs) as ref,
UNNEST(ref_types) as ref_type,
UNNEST(ref_roles) as ref_role,
UNNEST(range(length(refs))) as ref_idx
FROM ST_READOSM('monaco-latest.osm.pbf') r
SEMI JOIN matching_relations USING (id)
WHERE kind = 'relation'
)
SELECT id, ref, ref_role, ref_idx
FROM unnested_relation_refs
WHERE ref_type = 'way';

SELECT * FROM matching_relations_with_ways_refs;

┌──────────┬───────────┬──────────┬─────────┐
│ id │ ref │ ref_role │ ref_idx │
│ int64 │ int64 │ varchar │ int64 │
├──────────┼───────────┼──────────┼─────────┤
│ 7385 │ 30836152 │ outer │ 2 │
│ 7385 │ 889278953 │ outer │ 3 │
│ 7385 │ 889278956 │ outer │ 4 │
│ 7385 │ 889278957 │ outer │ 5 │
│ 7385 │ 889278958 │ outer │ 6 │
│ 7385 │ 889278962 │ outer │ 7 │
│ 7385 │ 960087656 │ outer │ 8 │
│ 7385 │ 30836155 │ outer │ 9 │
│ 7385 │ 889278965 │ outer │ 10 │
│ 7385 │ 889278963 │ outer │ 11 │
│ · │ · │ · │ · │
│ · │ · │ · │ · │
│ · │ · │ · │ · │
│ 11278320 │ 35150936 │ outer │ 197 │
│ 11278320 │ 466647567 │ outer │ 198 │
│ 11278320 │ 214008684 │ outer │ 199 │
│ 11278320 │ 466647569 │ outer │ 200 │
│ 11278320 │ 466647568 │ outer │ 201 │
│ 11278320 │ 214008689 │ outer │ 202 │
│ 11278320 │ 263776194 │ outer │ 203 │
│ 11278320 │ 31124893 │ outer │ 204 │
│ 11278320 │ 31124895 │ outer │ 205 │
│ 11278320 │ 31124899 │ outer │ 206 │
├──────────┴───────────┴──────────┴─────────┤
│ ? rows (>9999 rows, 20 shown) 4 columns │
└───────────────────────────────────────────┘

En el siguiente paso, construiremos cadenas lineales para las formas requeridas por las relaciones. La siguiente consulta comprime casi toda la lógica de lectura de formas de una sola vez (obteniendo los nodos necesarios, construyendo puntos y agrupándolos en cadenas de líneas):

CREATE TEMP TABLE required_ways_linestrings AS
WITH ways_required_by_relations_with_nodes_refs AS (
SELECT id, UNNEST(refs) as ref, UNNEST(range(length(refs))) as ref_idx
FROM ST_READOSM('monaco-latest.osm.pbf') ways
SEMI JOIN matching_relations_with_ways_refs
ON ways.id = matching_relations_with_ways_refs.ref
WHERE kind = 'way'
),
nodes_required_by_relations_with_geometries AS (
SELECT id, ST_POINT(lon, lat) geometry
FROM ST_READOSM('monaco-latest.osm.pbf') nodes
SEMI JOIN ways_required_by_relations_with_nodes_refs
ON nodes.id = ways_required_by_relations_with_nodes_refs.ref
WHERE kind = 'node'
)
SELECT
ways.id,
ST_MakeLine(list(nodes.geometry ORDER BY ref_idx ASC)) linestring
FROM ways_required_by_relations_with_nodes_refs ways
JOIN nodes_required_by_relations_with_geometries nodes
ON ways.ref = nodes.id
GROUP BY 1;

SELECT * FROM required_ways_linestrings;

┌────────────┬─────────────────────────────────────────────────────────────────┐
│ id │ linestring │
│ int64 │ geometry │
├────────────┼─────────────────────────────────────────────────────────────────┤
│ 37794470 │ LINESTRING (7.438411400000001 43.749422300000006, 7.4384056 4… │
│ 87878917 │ LINESTRING (7.418041100000001 43.7256933, 7.4181547 43.725613… │
│ 94252430 │ LINESTRING (7.4141873 43.729494100000004, 7.414203400000001 4… │
│ 94399618 │ LINESTRING (7.4239717 43.740543100000004, 7.4238252 43.740372… │
│ 94399736 │ LINESTRING (7.423881400000001 43.7402971, 7.4239411 43.740265… │
│ 104863462 │ LINESTRING (7.424840000000001 43.731281800000005, 7.424917000… │
│ 120114110 │ LINESTRING (7.420872 43.7370979, 7.4207787 43.7370534, 7.4206… │
│ 156242249 │ LINESTRING (7.4294728 43.739895600000004, 7.429579500000001 4… │
│ 165636038 │ LINESTRING (7.419717100000001 43.732398700000005, 7.419772900… │
│ 169297863 │ LINESTRING (7.422815300000001 43.7321967, 7.4234109 43.732263… │
│ · │ · │
│ · │ · │
│ · │ · │
│ 24874398 │ LINESTRING (7.418523400000001 43.7247599, 7.419012800000001 4… │
│ 92627424 │ LINESTRING (7.4166521 43.7322122, 7.4162303 43.73173910000000… │
│ 94452829 │ LINESTRING (7.4317066 43.7469647, 7.4317998 43.7470371, 7.431… │
│ 335740502 │ LINESTRING (7.4185151 43.7353633, 7.418442000000001 43.735298… │
│ 398377193 │ LINESTRING (7.420872 43.7370979, 7.420939000000001 43.7371298… │
│ 405529436 │ LINESTRING (7.417534900000001 43.7258914, 7.4175347 43.725833… │
│ 423632259 │ LINESTRING (7.4212208 43.7320944, 7.421461300000001 43.732057… │
│ 572935479 │ LINESTRING (7.427508100000001 43.7394168, 7.427496700000001 4… │
│ 586494707 │ LINESTRING (7.4270357 43.739109000000006, 7.4269512 43.739155… │
│ 1202570715 │ LINESTRING (7.426448400000001 43.739491400000006, 7.4264682 4… │
├────────────┴─────────────────────────────────────────────────────────────────┤
│ 141 rows (20 shown) 2 columns │
└──────────────────────────────────────────────────────────────────────────────┘

Después de crear las cadenas de líneas necesarias, ahora podemos unirlas con datos de relaciones. También nos aseguraremos de que los requisitos ref_role se analiza correctamente: complete los valores vacíos o reemplácelos si la relación se ha definido incorrectamente ref_roles en la base de datos de OSM.

CREATE TEMP TABLE matching_relations_with_ways_linestrings AS
WITH unnested_relations_with_way_linestrings AS (
SELECT
r.id,
COALESCE(r.ref_role, 'outer') as ref_role,
r.ref,
w.linestring::GEOMETRY as geometry
FROM matching_relations_with_ways_refs r
JOIN required_ways_linestrings w
ON w.id = r.ref
ORDER BY r.id, r.ref_idx
),
any_outer_refs AS (
-- check if any way attached to the relation has the `outer` role
SELECT id, bool_or(ref_role == 'outer') has_any_outer_refs
FROM unnested_relations_with_way_linestrings
GROUP BY id
)
SELECT
unnested_relations_with_way_linestrings.* EXCLUDE (ref_role),
-- if none of the way refs has `outer` role - treat each ref as `outer`
CASE WHEN any_outer_refs.has_any_outer_refs
THEN unnested_relations_with_way_linestrings.ref_role
ELSE 'outer'
END as ref_role
FROM unnested_relations_with_way_linestrings
JOIN any_outer_refs
ON any_outer_refs.id = unnested_relations_with_way_linestrings.id;

SELECT * FROM matching_relations_with_ways_linestrings;

┌──────────┬───────────┬────────────────────────────────────────────┬──────────┐
│ id │ ref │ geometry │ ref_role │
│ int64 │ int64 │ geometry │ varchar │
├──────────┼───────────┼────────────────────────────────────────────┼──────────┤
│ 7385 │ 772081595 │ LINESTRING (7.415291600000001 43.7234393… │ outer │
│ 7385 │ 212810311 │ LINESTRING (7.4120416 43.7280955, 7.4123… │ outer │
│ 7385 │ 176533407 │ LINESTRING (7.412670800000001 43.7316348… │ outer │
│ 7385 │ 37811853 │ LINESTRING (7.4127007 43.7346954, 7.4126… │ outer │
│ 7385 │ 37794471 │ LINESTRING (7.428754700000001 43.7460174… │ outer │
│ 7385 │ 398372186 │ LINESTRING (7.436885 43.7519173, 7.43677… │ outer │
│ 7385 │ 37794470 │ LINESTRING (7.438411400000001 43.7494223… │ outer │
│ 7385 │ 770774507 │ LINESTRING (7.439171000000001 43.7490109… │ outer │
│ 8654 │ 772081595 │ LINESTRING (7.415291600000001 43.7234393… │ outer │
│ 8654 │ 212810311 │ LINESTRING (7.4120416 43.7280955, 7.4123… │ outer │
│ · │ · │ · │ · │
│ · │ · │ · │ · │
│ · │ · │ · │ · │
│ 11546878 │ 840890844 │ LINESTRING (7.420754 43.735872900000004,… │ outer │
│ 11546878 │ 840890848 │ LINESTRING (7.4207413 43.7356673, 7.4207… │ outer │
│ 16467322 │ 772081595 │ LINESTRING (7.415291600000001 43.7234393… │ outer │
│ 16467322 │ 212810311 │ LINESTRING (7.4120416 43.7280955, 7.4123… │ outer │
│ 16467322 │ 176533407 │ LINESTRING (7.412670800000001 43.7316348… │ outer │
│ 16467322 │ 37811853 │ LINESTRING (7.4127007 43.7346954, 7.4126… │ outer │
│ 16467322 │ 37794471 │ LINESTRING (7.428754700000001 43.7460174… │ outer │
│ 16467322 │ 398372186 │ LINESTRING (7.436885 43.7519173, 7.43677… │ outer │
│ 16467322 │ 37794470 │ LINESTRING (7.438411400000001 43.7494223… │ outer │
│ 16467322 │ 770774507 │ LINESTRING (7.439171000000001 43.7490109… │ outer │
├──────────┴───────────┴────────────────────────────────────────────┴──────────┤
│ 410 rows (20 shown) 4 columns │
└──────────────────────────────────────────────────────────────────────────────┘

Como puede ver, hay varias formas de asignar cadenas lineales a cada relación. Veamos un ejemplo para ver cómo quedan en el mapa:

Una sola relación (5986437) con formas codificadas por colores que forman parte del mismo. Generado por el autor utilizando la biblioteca GeoPandas.

Para crear polígonos completos, tenemos que utilizar un ST_LineMerge función, que combinará una lista de cadenas lineales (puede compararla con unir trozos de cuerda). Puedes leer más sobre esta operación aquí:

Como paso de validación adicional, comprobaremos si las cadenas lineales producidas tienen al menos 4 puntos y si el primer punto es igual al último punto, antes de convertirlas en polígonos:

CREATE TEMP TABLE matching_relations_with_merged_polygons AS
WITH merged_linestrings AS (
SELECT
id,
ref_role,
UNNEST(
ST_Dump(ST_LineMerge(ST_Collect(list(geometry)))),
recursive := true
),
FROM matching_relations_with_ways_linestrings
GROUP BY id, ref_role
),
relations_with_linestrings AS (
SELECT
id,
ref_role,
-- ST_Dump returns column named `geom`
geom AS geometry,
row_number() OVER (PARTITION BY id) as geometry_id
FROM
merged_linestrings
-- discard linestrings with less than 4 points
WHERE ST_NPoints(geom) >= 4
),
valid_relations AS (
SELECT id, is_valid
FROM (
SELECT
id,
bool_and(
-- Check if start point equals the end point
ST_Equals(ST_StartPoint(geometry), ST_EndPoint(geometry))
) is_valid
FROM relations_with_linestrings
GROUP BY id
)
WHERE is_valid = true
)
SELECT
id,
ref_role,
ST_MakePolygon(geometry) geometry,
geometry_id
FROM relations_with_linestrings
SEMI JOIN valid_relations
ON relations_with_linestrings.id = valid_relations.id;

SELECT * FROM matching_relations_with_merged_polygons;

┌──────────┬──────────┬──────────────────────────────────────────┬─────────────┐
│ id │ ref_role │ geometry │ geometry_id │
│ int64 │ varchar │ geometry │ int64 │
├──────────┼──────────┼──────────────────────────────────────────┼─────────────┤
│ 1369631 │ inner │ POLYGON ((7.438232200000001 43.7511057… │ 1 │
│ 1369631 │ outer │ POLYGON ((7.438008600000001 43.7502850… │ 2 │
│ 1484217 │ outer │ POLYGON ((7.423010700000001 43.7307028… │ 1 │
│ 1484217 │ inner │ POLYGON ((7.423114600000001 43.7305994… │ 2 │
│ 5986436 │ outer │ POLYGON ((7.428754700000001 43.7460174… │ 1 │
│ 8269572 │ outer │ POLYGON ((7.4287048 43.737701400000006… │ 1 │
│ 8269572 │ inner │ POLYGON ((7.4284492 43.7375604, 7.4286… │ 2 │
│ 16248286 │ outer │ POLYGON ((7.426306200000001 43.7391565… │ 1 │
│ 16248286 │ inner │ POLYGON ((7.4263241 43.7390763, 7.4263… │ 2 │
│ 16261416 │ inner │ POLYGON ((7.417829200000001 43.731382,… │ 1 │
│ · │ · │ · │ · │
│ · │ · │ · │ · │
│ · │ · │ · │ · │
│ 8280869 │ inner │ POLYGON ((7.426753300000001 43.7388868… │ 2 │
│ 8280869 │ inner │ POLYGON ((7.4270357 43.739109000000006… │ 3 │
│ 8280869 │ inner │ POLYGON ((7.4271374 43.739011500000004… │ 4 │
│ 8280869 │ inner │ POLYGON ((7.4272666 43.7389165, 7.4273… │ 5 │
│ 11384697 │ outer │ POLYGON ((7.427075 43.7315167, 7.42749… │ 1 │
│ 11384697 │ inner │ POLYGON ((7.426917700000001 43.7313266… │ 2 │
│ 11384697 │ inner │ POLYGON ((7.427001400000001 43.7313937… │ 3 │
│ 11546878 │ outer │ POLYGON ((7.4207413 43.7356673, 7.4207… │ 1 │
│ 11538023 │ inner │ POLYGON ((7.426528200000001 43.7329359… │ 1 │
│ 11538023 │ outer │ POLYGON ((7.426554 43.7328276, 7.42654… │ 2 │
├──────────┴──────────┴──────────────────────────────────────────┴─────────────┤
│ 88 rows (20 shown) 4 columns │
└──────────────────────────────────────────────────────────────────────────────┘

Veamos el ejemplo anterior después de esta operación. Deberíamos esperar que estén presentes dos polígonos separados:

Una sola relación (5986437) con formas fusionadas como dos polígonos separados. Generado por el autor utilizando la biblioteca GeoPandas.

He mencionado anteriormente el outer y inner ‘ref_types’ de formas que crean una relación. Aquí puedes ver cómo luce:

Una sola relación (8280869) con vías fusionadas agrupadas en polígonos exteriores e interiores. Generado por el autor utilizando la biblioteca GeoPandas.

Los roles significan que el inner formas son los ‘agujeros’ dentro outer polígonos y tenemos que reproducir este paso para hacer geometrías adecuadas.

Centrémonos ahora en dividir los polígonos en grupos con y sin agujeros, usando el ST_Within predicado, que comprueba si una geometría está completamente dentro de otra.

CREATE TEMP TABLE matching_relations_with_outer_polygons_with_holes AS
WITH outer_polygons AS (
SELECT id, geometry_id, geometry
FROM matching_relations_with_merged_polygons
WHERE ref_role = 'outer'
), inner_polygons AS (
SELECT id, geometry_id, geometry
FROM matching_relations_with_merged_polygons
WHERE ref_role = 'inner'
)
SELECT
op.id,
op.geometry_id,
ST_Difference(any_value(op.geometry), ST_Union_Agg(ip.geometry)) geometry
FROM outer_polygons op
JOIN inner_polygons ip
ON op.id = ip.id AND ST_WITHIN(ip.geometry, op.geometry)
GROUP BY op.id, op.geometry_id;

CREATE TEMP TABLE matching_relations_with_outer_polygons_without_holes AS
WITH outer_polygons AS (
SELECT id, geometry_id, geometry
FROM matching_relations_with_merged_polygons
WHERE ref_role = 'outer'
)
SELECT
op.id,
op.geometry_id,
op.geometry
FROM outer_polygons op
ANTI JOIN matching_relations_with_outer_polygons_with_holes opwh
ON op.id = opwh.id AND op.geometry_id = opwh.geometry_id;

SELECT * FROM matching_relations_with_outer_polygons_with_holes
UNION ALL
SELECT * FROM matching_relations_with_outer_polygons_without_holes;

┌──────────┬─────────────┬─────────────────────────────────────────────────────┐
│ id │ geometry_id │ geometry │
│ int64 │ int64 │ geometry │
├──────────┼─────────────┼─────────────────────────────────────────────────────┤
│ 16248286 │ 1 │ POLYGON ((7.4263139 43.7391602, 7.426324900000001… │
│ 1369192 │ 1 │ POLYGON ((7.4226247 43.7402251, 7.4227848 43.7396… │
│ 8147763 │ 1 │ POLYGON ((7.438223000000001 43.7477296, 7.4382403… │
│ 393226 │ 1 │ POLYGON ((7.4204802 43.731016700000005, 7.4203979… │
│ 11484092 │ 4 │ POLYGON ((7.417896900000001 43.724827000000005, 7… │
│ 11384697 │ 1 │ POLYGON ((7.4274934 43.731250700000004, 7.4267196… │
│ 8269572 │ 1 │ POLYGON ((7.4287166 43.7376724, 7.4287948 43.7376… │
│ 16261416 │ 3 │ POLYGON ((7.4178309 43.731391, 7.417877000000001 … │
│ 16248284 │ 1 │ POLYGON ((7.426637500000001 43.7394678, 7.4266485… │
│ 16248285 │ 1 │ POLYGON ((7.426114600000001 43.739267600000005, 7… │
│ · │ · │ · │
│ · │ · │ · │
│ · │ · │ · │
│ 11546879 │ 1 │ POLYGON ((7.4209316 43.7356234, 7.421037 43.73562… │
│ 2220206 │ 1 │ POLYGON ((7.4120416 43.7280955, 7.412103900000001… │
│ 5986438 │ 1 │ POLYGON ((7.4191482 43.738887500000004, 7.4199833… │
│ 2221178 │ 1 │ POLYGON ((7.415860400000001 43.7313885, 7.416195 … │
│ 2221179 │ 1 │ POLYGON ((7.422280000000001 43.7367186, 7.4227584… │
│ 2220208 │ 1 │ POLYGON ((7.41849 43.730922400000004, 7.4185156 4… │
│ 2254506 │ 1 │ POLYGON ((7.432995900000001 43.7447102, 7.4329125… │
│ 5986437 │ 1 │ POLYGON ((7.4307593 43.745595, 7.4306621 43.74541… │
│ 5986437 │ 2 │ POLYGON ((7.4343358 43.7457085, 7.4341337 43.7455… │
│ 11546878 │ 1 │ POLYGON ((7.4207413 43.7356673, 7.4207413 43.7357… │
├──────────┴─────────────┴─────────────────────────────────────────────────────┤
│ 47 rows (20 shown) 3 columns │
└──────────────────────────────────────────────────────────────────────────────┘

En esta consulta, se utiliza otra combinación especial: ANTI JOIN. Éste filtra todas las filas de la tabla del lado izquierdo que están unidas por la tabla del lado derecho.

El último paso que se necesita es fusionar todos los polígonos para una sola relación usando el ST_Union_Agg operación. Combinará todos los polígonos en multipolígonos (si hay más de uno) y producirá una única geometría.

WITH unioned_outer_geometries AS (
SELECT id, geometry
FROM matching_relations_with_outer_polygons_with_holes
UNION ALL
SELECT id, geometry
FROM matching_relations_with_outer_polygons_without_holes
),
final_geometries AS (
SELECT id, ST_Union_Agg(geometry) geometry
FROM unioned_outer_geometries
GROUP BY id
)
SELECT r_g.id, r.tags, r_g.geometry
FROM final_geometries r_g
JOIN matching_relations r
ON r.id = r_g.id;

┌──────────┬──────────────────────┬────────────────────────────────────────────┐
│ id │ tags │ geometry │
│ int64 │ map(varchar, varch… │ geometry │
├──────────┼──────────────────────┼────────────────────────────────────────────┤
│ 393226 │ {building=castle, … │ POLYGON ((7.4204802 43.731016700000005, … │
│ 393481 │ {building=school, … │ POLYGON ((7.423992800000001 43.731841800… │
│ 1369191 │ {building=apartmen… │ POLYGON ((7.4231004 43.7412894, 7.423314… │
│ 1369192 │ {building=apartmen… │ POLYGON ((7.4226247 43.7402251, 7.422784… │
│ 1369193 │ {building=apartmen… │ POLYGON ((7.424 43.7405491, 7.4240401 43… │
│ 1369195 │ {building=apartmen… │ POLYGON ((7.418679900000001 43.738187100… │
│ 1369631 │ {addr:housenumber=… │ POLYGON ((7.4379729 43.7502505, 7.437937… │
│ 1369632 │ {addr:city=Monaco,… │ POLYGON ((7.4317121 43.74709, 7.4318277 … │
│ 1484190 │ {addr:city=Monaco,… │ POLYGON ((7.425469 43.731494000000005, 7… │
│ 1484217 │ {building=retail, … │ POLYGON ((7.423202300000001 43.7307117, … │
│ · │ · │ · │
│ · │ · │ · │
│ · │ · │ · │
│ 14399505 │ {area=yes, floatin… │ POLYGON ((7.4195986 43.7265293, 7.419595… │
│ 16248281 │ {building=apartmen… │ POLYGON ((7.4260438 43.739789800000004, … │
│ 16248282 │ {building=apartmen… │ POLYGON ((7.426243100000001 43.7396824, … │
│ 16248283 │ {building=apartmen… │ POLYGON ((7.426438200000001 43.739575200… │
│ 16248284 │ {building=apartmen… │ POLYGON ((7.426637500000001 43.7394678, … │
│ 16248285 │ {building=apartmen… │ POLYGON ((7.426114600000001 43.739267600… │
│ 16248286 │ {building=apartmen… │ POLYGON ((7.4263139 43.7391602, 7.426324… │
│ 16250182 │ {addr:country=MC, … │ POLYGON ((7.418348300000001 43.7256979, … │
│ 16261416 │ {natural=water, ty… │ POLYGON ((7.4178309 43.731391, 7.4178770… │
│ 11546878 │ {addr:city=Monaco,… │ POLYGON ((7.4207413 43.7356673, 7.420741… │
├──────────┴──────────────────────┴────────────────────────────────────────────┤
│ 46 rows (20 shown) 3 columns │
└──────────────────────────────────────────────────────────────────────────────┘

Aquí está el ejemplo de relación anterior, ahora con agujeros:

Una sola relación (8280869) — un polígono con agujeros. Generado por el autor utilizando la biblioteca GeoPandas.