]> git.openstreetmap.org Git - nominatim.git/blob - lib-sql/functions/place_triggers.sql
ignore countries without geometry or country code for location_area
[nominatim.git] / lib-sql / functions / place_triggers.sql
1 -- SPDX-License-Identifier: GPL-2.0-only
2 --
3 -- This file is part of Nominatim. (https://nominatim.org)
4 --
5 -- Copyright (C) 2026 by the Nominatim developer community.
6 -- For a full list of authors see the git log.
7
8 CREATE OR REPLACE FUNCTION place_insert()
9   RETURNS TRIGGER
10   AS $$
11 DECLARE
12   i INTEGER;
13   country RECORD;
14   existing RECORD;
15   existingplacex RECORD;
16   existingline BIGINT[];
17   interpol RECORD;
18 BEGIN
19   {% if debug %}
20     RAISE WARNING 'place_insert: % % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type,st_area(NEW.geometry);
21   {% endif %}
22
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),
30               null, NEW.geometry);
31     {% if debug %}
32       RAISE WARNING 'Invalid Geometry: % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type;
33     {% endif %}
34     RETURN null;
35   END IF;
36
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;
41
42   -- Have we already done this place?
43   SELECT * INTO existing
44     FROM place
45     WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
46           and class = NEW.class and type = NEW.type;
47
48   {% if debug %}RAISE WARNING 'Existing: %',existing.osm_id;{% endif %}
49
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;
52   END IF;
53
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;
57
58   -- ---- Interpolation Lines
59
60   IF NEW.class='place' and NEW.type='houses'
61      and NEW.osm_type='W' and ST_GeometryType(NEW.geometry) = 'ST_LineString'
62   THEN
63     PERFORM reinsert_interpolation(NEW.osm_id, NEW.address, NEW.geometry);
64
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;
71
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
76       THEN
77         UPDATE place
78           SET name = NEW.name,
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;
85        END IF;
86
87        RETURN NULL;
88     END IF;
89
90     RETURN NEW;
91   END IF;
92
93   -- ---- All other place types.
94
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
100   THEN
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);
108
109     RETURN null;
110   END IF;
111
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.)
117   IF NEW.osm_type='N'
118      and coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
119   THEN
120       FOR interpol IN
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)
129       LOOP
130         PERFORM reinsert_interpolation(interpol.osm_id, interpol.address,
131                                        interpol.geometry);
132       END LOOP;
133   END IF;
134
135   -- Get the existing placex entry.
136   SELECT * INTO existingplacex
137     FROM placex
138     WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
139           and class = NEW.class and type = NEW.type;
140
141   {% if debug %}RAISE WARNING 'Existing PlaceX: %',existingplacex.place_id;{% endif %}
142
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))
149   THEN
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;
159       IF i > 100000 THEN
160         RETURN null;
161       END IF;
162     END IF;
163     {% endif %}
164
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;
170     END IF;
171
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);
177
178     {% if debug %}RAISE WARNING 'insert done % % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type,NEW.name;{% endif %}
179
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
187       THEN
188         UPDATE place
189           SET name = NEW.name,
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;
196       END IF;
197
198       RETURN NULL;
199     END IF;
200
201     RETURN NEW;
202   END IF;
203
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')
209   THEN
210     -- Performance limit
211     IF ST_Area(NEW.geometry) < 0.000000001 AND ST_Area(existingplacex.geometry) < 1
212     THEN
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);
219
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);
224     END IF;
225   END IF;
226
227
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
234   THEN
235     UPDATE place
236       SET name = NEW.name,
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;
243
244     -- Boundaries must be areas.
245     IF NEW.class in ('boundary')
246        AND ST_GeometryType(NEW.geometry) not in ('ST_Polygon','ST_MultiPolygon')
247     THEN
248       DELETE FROM placex where place_id = existingplacex.place_id;
249       RETURN NULL;
250     END IF;
251
252     -- Update the placex entry in-place.
253     UPDATE placex
254       SET name = NEW.name,
255           address = NEW.address,
256           parent_place_id = null,
257           extratags = NEW.extratags,
258           admin_level = NEW.admin_level,
259           indexed_status = 2,
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;
264
265     -- Invalidate linked places: they potentially get a new name and addresses.
266     IF existingplacex.linked_place_id is not NULL THEN
267       UPDATE placex x
268         SET name = p.name,
269             extratags = p.extratags,
270             indexed_status = 2
271         FROM place p
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;
277     END IF;
278
279     -- Invalidate dependent objects effected by name changes
280     IF coalesce(existing.name::text, '') != coalesce(NEW.name::text, '')
281     THEN
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.
298       END IF;
299     END IF;
300   END IF;
301
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);
305   END IF;
306
307   -- Abort the insertion (we modified the existing place instead)
308   RETURN NULL;
309 END;
310 $$ LANGUAGE plpgsql;
311
312 CREATE OR REPLACE FUNCTION place_delete()
313   RETURNS TRIGGER
314   AS $$
315 DECLARE
316   deferred BOOLEAN;
317 BEGIN
318   {% if debug %}RAISE WARNING 'Delete for % % %/%', OLD.osm_type, OLD.osm_id, OLD.class, OLD.type;{% endif %}
319
320   deferred := ST_IsValid(OLD.geometry) and ST_Area(OLD.geometry) > 2;
321   IF deferred THEN
322     SELECT bool_or(not (rank_address = 0 or rank_address > 25)) INTO deferred
323       FROM placex
324       WHERE osm_type = OLD.osm_type and osm_id = OLD.osm_id
325             and class = OLD.class and type = OLD.type;
326   END IF;
327
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);
330
331   RETURN NULL;
332 END;
333 $$ LANGUAGE plpgsql;