1 -- SPDX-License-Identifier: GPL-2.0-only
3 -- This file is part of Nominatim. (https://nominatim.org)
5 -- Copyright (C) 2026 by the Nominatim developer community.
6 -- For a full list of authors see the git log.
8 CREATE OR REPLACE FUNCTION place_insert()
15 existingplacex RECORD;
16 existingline BIGINT[];
20 RAISE WARNING 'place_insert: % % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type,st_area(NEW.geometry);
23 -- Filter tuples with bad geometries.
24 IF ST_IsEmpty(NEW.geometry) OR NOT ST_IsValid(NEW.geometry) THEN
25 INSERT INTO import_polygon_error (osm_type, osm_id, class, type, name,
26 country_code, updated, errormessage,
27 prevgeometry, newgeometry)
28 VALUES (NEW.osm_type, NEW.osm_id, NEW.class, NEW.type, NEW.name,
29 NEW.address->'country', now(), ST_IsValidReason(NEW.geometry),
32 RAISE WARNING 'Invalid Geometry: % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type;
37 -- Remove the place from the list of places to be deleted
38 DELETE FROM place_to_be_deleted pdel
39 WHERE pdel.osm_type = NEW.osm_type and pdel.osm_id = NEW.osm_id
40 and pdel.class = NEW.class and pdel.type = NEW.type;
42 -- Have we already done this place?
43 SELECT * INTO existing
45 WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
46 and class = NEW.class and type = NEW.type;
48 {% if debug %}RAISE WARNING 'Existing: %',existing.osm_id;{% endif %}
50 IF existing.osm_type IS NULL THEN
51 DELETE FROM place where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class;
54 -- Remove any old logged data.
55 DELETE from import_polygon_error where osm_type = NEW.osm_type and osm_id = NEW.osm_id;
56 DELETE from import_polygon_delete where osm_type = NEW.osm_type and osm_id = NEW.osm_id;
58 -- ---- Interpolation Lines
60 IF NEW.class='place' and NEW.type='houses'
61 and NEW.osm_type='W' and ST_GeometryType(NEW.geometry) = 'ST_LineString'
63 PERFORM reinsert_interpolation(NEW.osm_id, NEW.address, NEW.geometry);
65 -- Now invalidate all address nodes on the line.
66 -- They get their parent from the interpolation.
67 UPDATE placex p SET indexed_status = 2
68 FROM planet_osm_ways w
69 WHERE w.id = NEW.osm_id and p.osm_type = 'N' and p.osm_id = any(w.nodes)
70 and indexed_status = 0;
72 -- If there is already an entry in place, just update that, if necessary.
73 IF existing.osm_type is not null THEN
74 IF coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
75 OR existing.geometry::text != NEW.geometry::text
79 address = NEW.address,
80 extratags = NEW.extratags,
81 admin_level = NEW.admin_level,
82 geometry = NEW.geometry
83 WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
84 and class = NEW.class and type = NEW.type;
93 -- ---- All other place types.
95 -- When an area is changed from large to small: log and discard change
96 IF existing.geometry is not null AND ST_IsValid(existing.geometry)
97 AND ST_Area(existing.geometry) > 0.02
98 AND ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon')
99 AND ST_Area(NEW.geometry) < ST_Area(existing.geometry) * 0.5
101 INSERT INTO import_polygon_error (osm_type, osm_id, class, type, name,
102 country_code, updated, errormessage,
103 prevgeometry, newgeometry)
104 VALUES (NEW.osm_type, NEW.osm_id, NEW.class, NEW.type, NEW.name,
105 NEW.address->'country', now(),
106 'Area reduced from '||st_area(existing.geometry)||' to '||st_area(NEW.geometry),
107 existing.geometry, NEW.geometry);
112 -- If an address node is part of a interpolation line and changes or is
113 -- newly inserted (happens when the node already existed but now gets address
114 -- information), then mark the interpolation line for reparenting.
115 -- (Already here, because interpolation lines are reindexed before nodes,
116 -- so in the second call it would be too late.)
118 and coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
121 SELECT DISTINCT osm_id, address, geometry FROM place, planet_osm_ways w
122 WHERE NEW.geometry && place.geometry
123 and place.osm_type = 'W'
124 and place.address ? 'interpolation'
125 and exists (SELECT * FROM location_property_osmline
126 WHERE osm_id = place.osm_id
127 and indexed_status in (0, 2))
128 and w.id = place.osm_id and NEW.osm_id = any (w.nodes)
130 PERFORM reinsert_interpolation(interpol.osm_id, interpol.address,
135 -- Get the existing placex entry.
136 SELECT * INTO existingplacex
138 WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
139 and class = NEW.class and type = NEW.type;
141 {% if debug %}RAISE WARNING 'Existing PlaceX: %',existingplacex.place_id;{% endif %}
143 -- To paraphrase: if there isn't an existing item, OR if the admin level has changed
144 IF existingplacex.osm_type IS NULL
145 or (existingplacex.class = 'boundary'
146 and ((coalesce(existingplacex.admin_level, 15) != coalesce(NEW.admin_level, 15)
147 and existingplacex.type = 'administrative')
148 or existingplacex.type != NEW.type))
150 {% if config.get_bool('LIMIT_REINDEXING') %}
151 -- sanity check: ignore admin_level changes on places with too many active children
152 -- or we end up reindexing entire countries because somebody accidentally deleted admin_level
153 IF existingplacex.osm_type IS NOT NULL THEN
154 SELECT count(*) INTO i FROM
155 (SELECT 'a' FROM placex, place_addressline
156 WHERE address_place_id = existingplacex.place_id
157 and placex.place_id = place_addressline.place_id
158 and indexed_status = 0 and place_addressline.isaddress LIMIT 100001) sub;
165 IF existingplacex.osm_type is not NULL THEN
166 -- Mark any existing place for delete in the placex table
167 UPDATE placex SET indexed_status = 100
168 WHERE placex.osm_type = NEW.osm_type and placex.osm_id = NEW.osm_id
169 and placex.class = NEW.class and placex.type = NEW.type;
172 -- Process it as a new insertion
173 INSERT INTO placex (osm_type, osm_id, class, type, name,
174 admin_level, address, extratags, geometry)
175 VALUES (NEW.osm_type, NEW.osm_id, NEW.class, NEW.type, NEW.name,
176 NEW.admin_level, NEW.address, NEW.extratags, NEW.geometry);
178 {% if debug %}RAISE WARNING 'insert done % % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type,NEW.name;{% endif %}
180 IF existing.osm_type is not NULL THEN
181 -- If there is already an entry in place, just update that, if necessary.
182 IF coalesce(existing.name, ''::hstore) != coalesce(NEW.name, ''::hstore)
183 or coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
184 or coalesce(existing.extratags, ''::hstore) != coalesce(NEW.extratags, ''::hstore)
185 or coalesce(existing.admin_level, 15) != coalesce(NEW.admin_level, 15)
186 or existing.geometry::text != NEW.geometry::text
190 address = NEW.address,
191 extratags = NEW.extratags,
192 admin_level = NEW.admin_level,
193 geometry = NEW.geometry
194 WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
195 and class = NEW.class and type = NEW.type;
204 -- Special case for polygon shape changes because they tend to be large
205 -- and we can be a bit clever about how we handle them
206 IF existing.geometry::text != NEW.geometry::text
207 AND ST_GeometryType(existing.geometry) in ('ST_Polygon','ST_MultiPolygon')
208 AND ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon')
211 IF ST_Area(NEW.geometry) < 0.000000001 AND ST_Area(existingplacex.geometry) < 1
213 -- re-index points that have moved in / out of the polygon.
214 -- Could be done as a single query but postgres gets the index usage wrong.
215 update placex set indexed_status = 2 where indexed_status = 0
216 AND ST_Intersects(NEW.geometry, placex.geometry)
217 AND NOT ST_Intersects(existingplacex.geometry, placex.geometry)
218 AND rank_search > existingplacex.rank_search AND (rank_search < 28 or name is not null);
220 update placex set indexed_status = 2 where indexed_status = 0
221 AND ST_Intersects(existingplacex.geometry, placex.geometry)
222 AND NOT ST_Intersects(NEW.geometry, placex.geometry)
223 AND rank_search > existingplacex.rank_search AND (rank_search < 28 or name is not null);
228 -- Has something relevant changed?
229 IF coalesce(existing.name::text, '') != coalesce(NEW.name::text, '')
230 OR coalesce(existing.extratags::text, '') != coalesce(NEW.extratags::text, '')
231 OR coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
232 OR coalesce(existing.admin_level, 15) != coalesce(NEW.admin_level, 15)
233 OR existing.geometry::text != NEW.geometry::text
237 address = NEW.address,
238 extratags = NEW.extratags,
239 admin_level = NEW.admin_level,
240 geometry = NEW.geometry
241 WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
242 and class = NEW.class and type = NEW.type;
244 -- Boundaries must be areas.
245 IF NEW.class in ('boundary')
246 AND ST_GeometryType(NEW.geometry) not in ('ST_Polygon','ST_MultiPolygon')
248 DELETE FROM placex where place_id = existingplacex.place_id;
252 -- Update the placex entry in-place.
255 address = NEW.address,
256 parent_place_id = null,
257 extratags = NEW.extratags,
258 admin_level = NEW.admin_level,
260 geometry = CASE WHEN existingplacex.rank_address = 0
261 THEN simplify_large_polygons(NEW.geometry)
262 ELSE NEW.geometry END
263 WHERE place_id = existingplacex.place_id;
265 -- Invalidate linked places: they potentially get a new name and addresses.
266 IF existingplacex.linked_place_id is not NULL THEN
269 extratags = p.extratags,
272 WHERE x.place_id = existingplacex.linked_place_id
273 and x.indexed_status = 0
274 and x.osm_type = p.osm_type
275 and x.osm_id = p.osm_id
276 and x.class = p.class;
279 -- Invalidate dependent objects effected by name changes
280 IF coalesce(existing.name::text, '') != coalesce(NEW.name::text, '')
282 IF existingplacex.rank_address between 26 and 27 THEN
283 -- When streets change their name, this may have an effect on POI objects
284 -- with addr:street tags.
285 UPDATE placex SET indexed_status = 2
286 WHERE indexed_status = 0 and address ? 'street'
287 and parent_place_id = existingplacex.place_id;
288 UPDATE placex SET indexed_status = 2
289 WHERE indexed_status = 0 and rank_search = 30 and address ? 'street'
290 and ST_DWithin(NEW.geometry, geometry, 0.002);
291 ELSEIF existingplacex.rank_address between 16 and 25 THEN
292 -- When places change their name, this may have an effect on POI objects
293 -- with addr:place tags.
294 UPDATE placex SET indexed_status = 2
295 WHERE indexed_status = 0 and address ? 'place' and rank_search = 30
296 and parent_place_id = existingplacex.place_id;
297 -- No update of surrounding objects, potentially too expensive.
302 -- When an existing way is updated, recalculate entrances
303 IF existingplacex.osm_type = 'W' and (existingplacex.rank_search > 27 or existingplacex.class IN ('landuse', 'leisure')) THEN
304 PERFORM place_update_entrances(existingplacex.place_id, existingplacex.osm_id);
307 -- Abort the insertion (we modified the existing place instead)
312 CREATE OR REPLACE FUNCTION place_delete()
318 {% if debug %}RAISE WARNING 'Delete for % % %/%', OLD.osm_type, OLD.osm_id, OLD.class, OLD.type;{% endif %}
320 deferred := ST_IsValid(OLD.geometry) and ST_Area(OLD.geometry) > 2;
322 SELECT bool_or(not (rank_address = 0 or rank_address > 25)) INTO deferred
324 WHERE osm_type = OLD.osm_type and osm_id = OLD.osm_id
325 and class = OLD.class and type = OLD.type;
328 INSERT INTO place_to_be_deleted (osm_type, osm_id, class, type, deferred)
329 VALUES(OLD.osm_type, OLD.osm_id, OLD.class, OLD.type, deferred);