Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions images/tiler-imposm/config/imposm3.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,6 @@
"odbl:note",
"history",

"roof:*",
"building:levels",
"building:part",
"building:material",
"building:colour",
"generator:*",
"tactile_paving",
"crossing:markings",
Expand Down
60 changes: 60 additions & 0 deletions images/tiler-imposm/config/layers/buildings.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,66 @@
"name": "height",
"key": "height"
},
{
"type": "string",
"name": "min_height",
"key": "min_height"
},
{
"type": "string",
"name": "building_height",
"key": "building:height"
},
{
"type": "string",
"name": "building_min_level",
"key": "building:min_level"
},
{
"type": "string",
"name": "building_use",
"key": "building:use"
},
{
"type": "string",
"name": "building_material",
"key": "building:material"
},
{
"type": "string",
"name": "building_levels",
"key": "building:levels"
},
{
"type": "string",
"name": "building_colour",
"key": "building:colour"
},
{
"type": "string",
"name": "building_part",
"key": "building:part"
},
Comment on lines +79 to +83
Copy link
Copy Markdown
Member

@1ec5 1ec5 May 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike all the other keys, building:part=* does not require building=*. It represents a different kind of feature. For example, this dome doesn’t appear in the staging tiles. We can include building parts in the buildings layer for convenience, rather than a separate layer as suggested in OpenHistoricalMap/issues#1370, but we’d need a boolean attribute indicating that it’s a building part.

In other tile schemas that expose building parts, a building outline has an attribute that tells the stylesheet not to extrude it in 3D. This attribute would appear on any building=* area that contains building parts or that has the role outline in a building relation. Without this attribute, the extruded building outline would obscure any building parts within it.

{
"type": "string",
"name": "roof_material",
"key": "roof:material"
},
{
"type": "string",
"name": "roof_colour",
"key": "roof:colour"
},
{
"type": "string",
"name": "roof_shape",
"key": "roof:shape"
},
{
"type": "string",
"name": "roof_height",
"key": "roof:height"
},
{
"type": "string",
"name": "start_date",
Expand Down
5 changes: 5 additions & 0 deletions images/tiler-imposm/config/layers/buildings_points.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
"name": "class",
"key": null
},
{
"type": "string",
"name": "building_use",
"key": "building:use"
},
{
"type": "string",
"name": "start_date",
Expand Down
89 changes: 89 additions & 0 deletions images/tiler-imposm/config/layers/water_multilines.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"tags": {
"load_all": true,
"exclude": [
"created_by",
"source",
"source:datetime"
]
},
"generalized_tables": {},
"tables": {
"water_multilines": {
"type": "relation_member",
"fields": [
{
"type": "id",
"name": "osm_id",
"key": null
},
{
"type": "geometry",
"name": "geometry",
"key": null
},
{
"type": "string",
"name": "name",
"key": "name"
},
{
"type": "mapping_value",
"name": "type",
"key": null
},
{
"type": "mapping_key",
"name": "class",
"key": null
},
{
"type": "string",
"name": "bridge",
"key": "bridge"
},
{
"type": "string",
"name": "start_date",
"key": "start_date"
},
{
"type": "string",
"name": "end_date",
"key": "end_date"
},
{
"type": "hstore_tags",
"name": "tags",
"key": null
},
{
"type": "member_id",
"name": "member"
},
{
"type": "hstore_tags",
"name": "me_tags",
"from_member": true
},
{
"type": "string",
"name": "me_name",
"key": "name",
"from_member": true
},
{
"type": "string",
"name": "me_waterway",
"key": "waterway",
"from_member": true
}
],
"mapping": {
"type": [
"waterway"
]
}
}
}
}
70 changes: 43 additions & 27 deletions images/tiler-imposm/queries/ohm_mviews/buildings.sql
Original file line number Diff line number Diff line change
@@ -1,61 +1,77 @@
-- ============================================================================
-- Prepare points materialized view for higher zoom levels (12+)
-- Add height and height_fixed columns
-- All building/roof attributes (height, building:height, building:material, etc.)
-- are now extracted as native columns by imposm in osm_buildings_points
-- ============================================================================
-- 3D / rendering attributes (height, materials, colours, levels, parts, roof
-- shape) are only meaningful on polygon footprints. Points (building=*
-- nodes) keep just identity/classification fields (name, type, building_use,
-- addresses). Inject NULLs of matching types so the UNION with polygon
-- centroids in mv_buildings_points_centroids_* aligns by name and type.
SELECT create_points_mview(
'osm_buildings_points',
'mv_buildings_points',
'id, source, osm_id',
ARRAY['NULL as height'],
ARRAY[
'NULL::double precision AS height',
'NULL::double precision AS min_height',
'NULL::double precision AS building_height',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

building:height=* is deprecated in favor of height=*. I just eliminated the last occurrence in the database.

'NULL::double precision AS roof_height',
'NULL::text AS building_min_level',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

building:min_level=* is also an integer.

'NULL::text AS building_material',
'NULL::text AS building_levels',
'NULL::text AS building_colour',
'NULL::text AS building_part',
'NULL::text AS roof_material',
'NULL::text AS roof_colour',
'NULL::text AS roof_shape'
],
NULL
);



-- ============================================================================
-- Zoom 14-15:
-- Very low simplification (5m)
-- Very small areas (>5K m² = 0.005 km²)
-- Add height_fixed column
-- Zoom 16-20: BASE
-- No simplification, all areas. Single point where height/building_height/
-- roof_height are parsed to numeric. All derived zoom levels inherit this.
-- ============================================================================
SELECT create_areas_mview(
'osm_buildings',
'mv_buildings_areas_z14_15',
5,
5000,
'mv_buildings_areas_z16_20',
0,
0,
'id, osm_id, type',
NULL,
NULL,
NULL
'{"height": "parse_to_meters(height)", "min_height": "parse_to_meters(min_height)", "building_height": "parse_to_meters(building_height)", "roof_height": "parse_to_meters(roof_height)"}'::jsonb
);

SELECT create_points_centroids_mview(
'mv_buildings_areas_z14_15',
'mv_buildings_points_centroids_z14_15',
'mv_buildings_points'
);
-- ============================================================================
-- Zoom 16-20:
-- No simplification
-- All areas
-- Add height_fixed column
-- Zoom 14-15: derived from z16_20
-- Low simplification (5m), filters out small buildings (<5,000 m²)
-- ============================================================================

SELECT create_areas_mview(
'osm_buildings',
SELECT create_area_mview_from_mview(
'mv_buildings_areas_z16_20',
0,
0,
'id, osm_id, type',
NULL,
NULL,
'mv_buildings_areas_z14_15',
5,
5000,
NULL
);

-- ============================================================================
-- Centroids per zoom level (UNION with point-tagged buildings)
-- ============================================================================
SELECT create_points_centroids_mview(
'mv_buildings_areas_z16_20',
'mv_buildings_points_centroids_z16_20',
'mv_buildings_points'
);
SELECT create_points_centroids_mview(
'mv_buildings_areas_z14_15',
'mv_buildings_points_centroids_z14_15',
'mv_buildings_points'
);

-- Refresh areas views
-- REFRESH MATERIALIZED VIEW CONCURRENTLY mv_buildings_areas_z14_15;
Expand Down
54 changes: 54 additions & 0 deletions images/tiler-imposm/queries/utils/postgis_helpers.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,58 @@ END;
$$ STRICT
LANGUAGE plpgsql IMMUTABLE;

-- Parse free-form OSM height-like values (height=*, min_height=*, roof:height=*,
-- building:height=*) to meters as double precision.
--
-- Accepts (per https://wiki.openstreetmap.org/wiki/Key:height):
-- "20", "20.5" -> meters (OSM default unit)
-- "20 m", "20m" -> meters with explicit unit
-- "20 meter", "20 meters"
-- "85'", "85 ft" -> feet -> meters
-- "8'5\"", "8'5" -> feet + inches -> meters
--
-- Returns NULL for null/empty/unparseable input or for non-positive / out-of-range
-- values (<=0 or >1000m). NULLs are stripped from MVT properties, so consumers
-- can use ["coalesce", ["get","height"], <fallback>] safely. Why: a 0 is a real
-- numeric in MVT and would short-circuit coalesce, rendering 3D extrusions flat.
CREATE OR REPLACE FUNCTION parse_to_meters(input text) RETURNS double precision AS $$
DECLARE
s text;
ft numeric;
inch numeric;
m text;
result double precision;
BEGIN
s := trim(input);
IF s = '' THEN
RETURN NULL;
END IF;

-- feet + optional inches: 8'5" / 8'5 / 8'
IF s ~ '^\d+(\.\d+)?''(\s*\d+(\.\d+)?"?)?$' THEN
ft := (regexp_match(s, '^(\d+(\.\d+)?)'''))[1]::numeric;
inch := COALESCE((regexp_match(s, '''\s*(\d+(\.\d+)?)"?$'))[1], '0')::numeric;
result := (ft * 0.3048) + (inch * 0.0254);
-- feet with ft suffix: 85ft / 85 ft / 85.5 ft
ELSIF s ~* '^\d+(\.\d+)?\s*ft$' THEN
ft := regexp_replace(s, '\s*ft$', '', 'i')::numeric;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit symbols are case-sensitive.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation discourages ft, and we don’t currently have any occurrences in the database. OSM’s U.S. community has frequently discussed making it acceptable again, but for now, 8' is much more common. If we support it, we should probably support 8 ft 5 in too.

result := ft * 0.3048;
-- meters (default or explicit): 20 / 20.5 / 20 m / 20m / 20 meters
ELSIF s ~* '^-?\d+(\.\d+)?\s*(m|meter|meters)?$' THEN
m := regexp_replace(s, '\s*(m|meter|meters)\s*$', '', 'i');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The meter and meters symbols are discouraged. We only had one occurrence, which I’ve eliminated.

result := m::double precision;
ELSE
RETURN NULL;
END IF;

IF result <= 0 OR result > 1000 THEN
RETURN NULL;
END IF;
RETURN result;
EXCEPTION WHEN others THEN
RETURN NULL;
END;
$$ STRICT
LANGUAGE plpgsql IMMUTABLE;

COMMIT;
15 changes: 11 additions & 4 deletions images/tiler-imposm/queries/utils/utils.sql
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,22 @@ $$ LANGUAGE plpgsql;
-- Function: get_language_columns()
-- Description:
-- Returns a comma-separated list of SQL expressions like:
-- tags -> 'name:es' AS "es"
-- Based on aliases found in the `languages` table.
-- tags -> 'name:zh-Hant-TW' AS "name_zh-Hant-TW"
-- Based on the `key_name` values stored in the `languages` table.
--
-- Only the first ':' in the tag key (the one separating `name` from the
-- language tag) is rewritten to `_`. Hyphens and mixed case inside the
-- IETF BCP 47 language tag are preserved so consumers (e.g. Diplomat) can
-- discover script and country subtags. Identifiers are quoted with %I so
-- that hyphens / uppercase are accepted as PostgreSQL column names.
--
-- Notes:
-- - Designed for direct use when `tags` is accessed without a table alias.
-- - Useful for generating multilingual columns dynamically in SQL queries.
--
-- Example:
-- get_language_columns() → "tags->'name:es' AS es, tags->'name:fr' AS fr, ..."
-- get_language_columns() → tags->'name:es' AS "name_es",
-- tags->'name:zh-Hant-TW' AS "name_zh-Hant-TW", ...
-- ============================================================================
CREATE OR REPLACE FUNCTION get_language_columns()
RETURNS TEXT AS $$
Expand All @@ -98,7 +105,7 @@ BEGIN
format(
'tags -> %L AS %I',
key_name,
'name_' || regexp_replace(lower(substring(key_name from 6)), '[^a-z0-9]', '_', 'g')
'name_' || substring(key_name from 6)
),
', '
)
Expand Down
4 changes: 4 additions & 0 deletions images/tiler-imposm/scripts/create_mviews.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ fi
##################### OHM #####################
log_message "Creating materialized views for OSM data"

# Always (re)load helper functions so mview SQL can rely on them after
# upgrades or reimports without re-running the full --all path.
execute_sql_file ./queries/utils/postgis_helpers.sql

## Admin boundaries areas
execute_sql_file queries/ohm_mviews/admin_boundaries_areas.sql
execute_sql_file queries/ohm_mviews/admin_boundaries_centroids.sql
Expand Down
Loading