]> git.openstreetmap.org Git - nominatim.git/blob - lib-sql/functions/place_triggers.sql
Merge remote-tracking branch 'upstream/master'
[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) 2022 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;
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
71     -- If there is already an entry in place, just update that, if necessary.
72     IF existing.osm_type is not null THEN
73       IF coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
74          OR existing.geometry::text != NEW.geometry::text
75       THEN
76         UPDATE place
77           SET name = NEW.name,
78               address = NEW.address,
79               extratags = NEW.extratags,
80               admin_level = NEW.admin_level,
81               geometry = NEW.geometry
82           WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
83                 and class = NEW.class and type = NEW.type;
84        END IF;
85
86        RETURN NULL;
87     END IF;
88
89     RETURN NEW;
90   END IF;
91
92   -- ---- Postcode points.
93
94   IF NEW.class = 'place' AND NEW.type = 'postcode' THEN
95     -- Pure postcodes are never queried from placex so we don't add them.
96     -- location_postcodes is filled from the place table directly.
97
98     -- Remove any old placex entry.
99     DELETE FROM placex WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id;
100
101     IF existing.osm_type IS NOT NULL THEN
102       IF coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
103          OR existing.geometry::text != NEW.geometry::text
104       THEN
105         UPDATE place
106           SET name = NEW.name,
107               address = NEW.address,
108               extratags = NEW.extratags,
109               admin_level = NEW.admin_level,
110               geometry = NEW.geometry
111           WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
112                 and class = NEW.class and type = NEW.type;
113       END IF;
114
115       RETURN NULL;
116     END IF;
117
118     RETURN NEW;
119   END IF;
120
121   -- ---- All other place types.
122
123   -- When an area is changed from large to small: log and discard change
124   IF existing.geometry is not null AND ST_IsValid(existing.geometry)
125     AND ST_Area(existing.geometry) > 0.02
126     AND ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon')
127     AND ST_Area(NEW.geometry) < ST_Area(existing.geometry) * 0.5
128   THEN
129     INSERT INTO import_polygon_error (osm_type, osm_id, class, type, name,
130                                       country_code, updated, errormessage,
131                                       prevgeometry, newgeometry)
132       VALUES (NEW.osm_type, NEW.osm_id, NEW.class, NEW.type, NEW.name,
133               NEW.address->'country', now(),
134               'Area reduced from '||st_area(existing.geometry)||' to '||st_area(NEW.geometry),
135               existing.geometry, NEW.geometry);
136
137     RETURN null;
138   END IF;
139
140   -- If an address node is part of a interpolation line and changes or is
141   -- newly inserted (happens when the node already existed but now gets address
142   -- information), then mark the interpolation line for reparenting.
143   -- (Already here, because interpolation lines are reindexed before nodes,
144   --  so in the second call it would be too late.)
145   IF NEW.osm_type='N'
146      and coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
147   THEN
148       FOR interpol IN
149         SELECT DISTINCT osm_id, address, geometry FROM place, planet_osm_ways w
150         WHERE NEW.geometry && place.geometry
151               and place.osm_type = 'W'
152               and place.address ? 'interpolation'
153               and exists (SELECT * FROM location_property_osmline
154                           WHERE osm_id = place.osm_id
155                                 and indexed_status in (0, 2))
156               and w.id = place.osm_id and NEW.osm_id = any (w.nodes)
157       LOOP
158         PERFORM reinsert_interpolation(interpol.osm_id, interpol.address,
159                                        interpol.geometry);
160       END LOOP;
161   END IF;
162
163   -- Get the existing placex entry.
164   SELECT * INTO existingplacex
165     FROM placex
166     WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
167           and class = NEW.class and type = NEW.type;
168
169   {% if debug %}RAISE WARNING 'Existing PlaceX: %',existingplacex.place_id;{% endif %}
170
171   -- To paraphrase: if there isn't an existing item, OR if the admin level has changed
172   IF existingplacex.osm_type IS NULL
173      or (existingplacex.class = 'boundary'
174          and ((coalesce(existingplacex.admin_level, 15) != coalesce(NEW.admin_level, 15)
175                and existingplacex.type = 'administrative')
176               or existingplacex.type != NEW.type))
177   THEN
178     {% if config.get_bool('LIMIT_REINDEXING') %}
179     -- sanity check: ignore admin_level changes on places with too many active children
180     -- or we end up reindexing entire countries because somebody accidentally deleted admin_level
181     IF existingplacex.osm_type IS NOT NULL THEN
182       SELECT count(*) INTO i FROM
183         (SELECT 'a' FROM placex, place_addressline
184           WHERE address_place_id = existingplacex.place_id
185                 and placex.place_id = place_addressline.place_id
186                 and indexed_status = 0 and place_addressline.isaddress LIMIT 100001) sub;
187       IF i > 100000 THEN
188         RETURN null;
189       END IF;
190     END IF;
191     {% endif %}
192
193     IF existingplacex.osm_type is not NULL THEN
194       -- Mark any existing place for delete in the placex table
195       UPDATE placex SET indexed_status = 100
196         WHERE placex.osm_type = NEW.osm_type and placex.osm_id = NEW.osm_id
197               and placex.class = NEW.class and placex.type = NEW.type;
198     END IF;
199
200     -- Process it as a new insertion
201     INSERT INTO placex (osm_type, osm_id, class, type, name,
202                         admin_level, address, extratags, geometry)
203       VALUES (NEW.osm_type, NEW.osm_id, NEW.class, NEW.type, NEW.name,
204               NEW.admin_level, NEW.address, NEW.extratags, NEW.geometry);
205
206     {% if debug %}RAISE WARNING 'insert done % % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type,NEW.name;{% endif %}
207
208     IF existing.osm_type is not NULL THEN
209       -- If there is already an entry in place, just update that, if necessary.
210       IF coalesce(existing.name, ''::hstore) != coalesce(NEW.name, ''::hstore)
211          or coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
212          or coalesce(existing.extratags, ''::hstore) != coalesce(NEW.extratags, ''::hstore)
213          or coalesce(existing.admin_level, 15) != coalesce(NEW.admin_level, 15)
214          or existing.geometry::text != NEW.geometry::text
215       THEN
216         UPDATE place
217           SET name = NEW.name,
218               address = NEW.address,
219               extratags = NEW.extratags,
220               admin_level = NEW.admin_level,
221               geometry = NEW.geometry
222           WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
223                 and class = NEW.class and type = NEW.type;
224       END IF;
225
226       RETURN NULL;
227     END IF;
228
229     RETURN NEW;
230   END IF;
231
232   -- Special case for polygon shape changes because they tend to be large
233   -- and we can be a bit clever about how we handle them
234   IF existing.geometry::text != NEW.geometry::text
235      AND ST_GeometryType(existing.geometry) in ('ST_Polygon','ST_MultiPolygon')
236      AND ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon')
237   THEN
238     -- Performance limit
239     IF ST_Area(NEW.geometry) < 0.000000001 AND ST_Area(existingplacex.geometry) < 1
240     THEN
241       -- re-index points that have moved in / out of the polygon.
242       -- Could be done as a single query but postgres gets the index usage wrong.
243       update placex set indexed_status = 2 where indexed_status = 0
244           AND ST_Intersects(NEW.geometry, placex.geometry)
245           AND NOT ST_Intersects(existingplacex.geometry, placex.geometry)
246           AND rank_search > existingplacex.rank_search AND (rank_search < 28 or name is not null);
247
248       update placex set indexed_status = 2 where indexed_status = 0
249           AND ST_Intersects(existingplacex.geometry, placex.geometry)
250           AND NOT ST_Intersects(NEW.geometry, placex.geometry)
251           AND rank_search > existingplacex.rank_search AND (rank_search < 28 or name is not null);
252     END IF;
253   END IF;
254
255
256   -- Has something relevant changed?
257   IF coalesce(existing.name::text, '') != coalesce(NEW.name::text, '')
258      OR coalesce(existing.extratags::text, '') != coalesce(NEW.extratags::text, '')
259      OR coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
260      OR coalesce(existing.admin_level, 15) != coalesce(NEW.admin_level, 15)
261      OR existing.geometry::text != NEW.geometry::text
262   THEN
263     UPDATE place
264       SET name = NEW.name,
265           address = NEW.address,
266           extratags = NEW.extratags,
267           admin_level = NEW.admin_level,
268           geometry = NEW.geometry
269       WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
270             and class = NEW.class and type = NEW.type;
271
272     -- Postcode areas are only kept, when there is an actual postcode assigned.
273     IF NEW.class = 'boundary' AND NEW.type = 'postal_code' THEN
274       IF NEW.address is NULL OR NOT NEW.address ? 'postcode' THEN
275         -- postcode was deleted, no longer retain in placex
276         DELETE FROM placex where place_id = existingplacex.place_id;
277         RETURN NULL;
278       END IF;
279
280       NEW.name := hstore('ref', NEW.address->'postcode');
281     END IF;
282
283     -- Boundaries must be areas.
284     IF NEW.class in ('boundary')
285        AND ST_GeometryType(NEW.geometry) not in ('ST_Polygon','ST_MultiPolygon')
286     THEN
287       DELETE FROM placex where place_id = existingplacex.place_id;
288       RETURN NULL;
289     END IF;
290
291     -- Update the placex entry in-place.
292     UPDATE placex
293       SET name = NEW.name,
294           address = NEW.address,
295           parent_place_id = null,
296           extratags = NEW.extratags,
297           admin_level = NEW.admin_level,
298           indexed_status = 2,
299           geometry = NEW.geometry
300       WHERE place_id = existingplacex.place_id;
301
302     -- Invalidate linked places: they potentially get a new name and addresses.
303     IF existingplacex.linked_place_id is not NULL THEN
304       UPDATE placex x
305         SET name = p.name,
306             extratags = p.extratags,
307             indexed_status = 2
308         FROM place p
309         WHERE x.place_id = existingplacex.linked_place_id
310               and x.indexed_status = 0
311               and x.osm_type = p.osm_type
312               and x.osm_id = p.osm_id
313               and x.class = p.class;
314     END IF;
315
316     -- Invalidate dependent objects effected by name changes
317     IF coalesce(existing.name::text, '') != coalesce(NEW.name::text, '')
318     THEN
319       IF existingplacex.rank_address between 26 and 27 THEN
320         -- When streets change their name, this may have an effect on POI objects
321         -- with addr:street tags.
322         UPDATE placex SET indexed_status = 2
323           WHERE indexed_status = 0 and address ? 'street'
324                 and parent_place_id = existingplacex.place_id;
325         UPDATE placex SET indexed_status = 2
326           WHERE indexed_status = 0 and rank_search = 30 and address ? 'street'
327                 and ST_DWithin(NEW.geometry, geometry, 0.002);
328       ELSEIF existingplacex.rank_address between 16 and 25 THEN
329         -- When places change their name, this may have an effect on POI objects
330         -- with addr:place tags.
331         UPDATE placex SET indexed_status = 2
332           WHERE indexed_status = 0 and address ? 'place' and rank_search = 30
333                 and parent_place_id = existingplacex.place_id;
334         -- No update of surrounding objects, potentially too expensive.
335       END IF;
336     END IF;
337   END IF;
338
339   -- Abort the insertion (we modified the existing place instead)
340   RETURN NULL;
341 END;
342 $$ LANGUAGE plpgsql;
343
344 CREATE OR REPLACE FUNCTION place_delete()
345   RETURNS TRIGGER
346   AS $$
347 DECLARE
348   deferred BOOLEAN;
349 BEGIN
350   {% if debug %}RAISE WARNING 'Delete for % % %/%', OLD.osm_type, OLD.osm_id, OLD.class, OLD.type;{% endif %}
351
352   deferred := ST_IsValid(OLD.geometry) and ST_Area(OLD.geometry) > 2;
353   IF deferred THEN
354     SELECT bool_or(not (rank_address = 0 or rank_address > 25)) INTO deferred
355       FROM placex
356       WHERE osm_type = OLD.osm_type and osm_id = OLD.osm_id
357             and class = OLD.class and type = OLD.type;
358   END IF;
359
360   INSERT INTO place_to_be_deleted (osm_type, osm_id, class, type, deferred)
361     VALUES(OLD.osm_type, OLD.osm_id, OLD.class, OLD.type, deferred);
362
363   RETURN NULL;
364 END;
365 $$ LANGUAGE plpgsql;
366
367 CREATE OR REPLACE FUNCTION flush_deleted_places()
368   RETURNS INTEGER
369   AS $$
370 BEGIN
371   -- deleting large polygons can have a massive effect on the system - require manual intervention to let them through
372   INSERT INTO import_polygon_delete (osm_type, osm_id, class, type)
373     SELECT osm_type, osm_id, class, type FROM place_to_be_deleted WHERE deferred;
374
375   -- delete from place table
376   ALTER TABLE place DISABLE TRIGGER place_before_delete;
377   DELETE FROM place USING place_to_be_deleted
378     WHERE place.osm_type = place_to_be_deleted.osm_type
379           and place.osm_id = place_to_be_deleted.osm_id
380           and place.class = place_to_be_deleted.class
381           and place.type = place_to_be_deleted.type
382           and not deferred;
383   ALTER TABLE place ENABLE TRIGGER place_before_delete;
384
385   -- Mark for delete in the placex table
386   UPDATE placex SET indexed_status = 100 FROM place_to_be_deleted
387     WHERE placex.osm_type = place_to_be_deleted.osm_type
388           and placex.osm_id = place_to_be_deleted.osm_id
389           and placex.class = place_to_be_deleted.class
390           and placex.type = place_to_be_deleted.type
391           and not deferred;
392
393    -- Mark for delete in interpolations
394    UPDATE location_property_osmline SET indexed_status = 100 FROM place_to_be_deleted
395     WHERE place_to_be_deleted.osm_type = 'W'
396           and place_to_be_deleted.class = 'place'
397           and place_to_be_deleted.type = 'houses'
398           and location_property_osmline.osm_id = place_to_be_deleted.osm_id
399           and not deferred;
400
401    -- Clear todo list.
402    TRUNCATE TABLE place_to_be_deleted;
403
404    RETURN NULL;
405 END;
406 $$ LANGUAGE plpgsql;
407