]> git.openstreetmap.org Git - nominatim.git/blob - lib-sql/functions/place_triggers.sql
Merge pull request #3519 from lonvia/api-error-handling
[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 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
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 = CASE WHEN existingplacex.rank_address = 0
300                           THEN simplify_large_polygons(NEW.geometry)
301                           ELSE NEW.geometry END
302       WHERE place_id = existingplacex.place_id;
303
304     -- Invalidate linked places: they potentially get a new name and addresses.
305     IF existingplacex.linked_place_id is not NULL THEN
306       UPDATE placex x
307         SET name = p.name,
308             extratags = p.extratags,
309             indexed_status = 2
310         FROM place p
311         WHERE x.place_id = existingplacex.linked_place_id
312               and x.indexed_status = 0
313               and x.osm_type = p.osm_type
314               and x.osm_id = p.osm_id
315               and x.class = p.class;
316     END IF;
317
318     -- Invalidate dependent objects effected by name changes
319     IF coalesce(existing.name::text, '') != coalesce(NEW.name::text, '')
320     THEN
321       IF existingplacex.rank_address between 26 and 27 THEN
322         -- When streets change their name, this may have an effect on POI objects
323         -- with addr:street tags.
324         UPDATE placex SET indexed_status = 2
325           WHERE indexed_status = 0 and address ? 'street'
326                 and parent_place_id = existingplacex.place_id;
327         UPDATE placex SET indexed_status = 2
328           WHERE indexed_status = 0 and rank_search = 30 and address ? 'street'
329                 and ST_DWithin(NEW.geometry, geometry, 0.002);
330       ELSEIF existingplacex.rank_address between 16 and 25 THEN
331         -- When places change their name, this may have an effect on POI objects
332         -- with addr:place tags.
333         UPDATE placex SET indexed_status = 2
334           WHERE indexed_status = 0 and address ? 'place' and rank_search = 30
335                 and parent_place_id = existingplacex.place_id;
336         -- No update of surrounding objects, potentially too expensive.
337       END IF;
338     END IF;
339   END IF;
340
341   -- Abort the insertion (we modified the existing place instead)
342   RETURN NULL;
343 END;
344 $$ LANGUAGE plpgsql;
345
346 CREATE OR REPLACE FUNCTION place_delete()
347   RETURNS TRIGGER
348   AS $$
349 DECLARE
350   deferred BOOLEAN;
351 BEGIN
352   {% if debug %}RAISE WARNING 'Delete for % % %/%', OLD.osm_type, OLD.osm_id, OLD.class, OLD.type;{% endif %}
353
354   deferred := ST_IsValid(OLD.geometry) and ST_Area(OLD.geometry) > 2;
355   IF deferred THEN
356     SELECT bool_or(not (rank_address = 0 or rank_address > 25)) INTO deferred
357       FROM placex
358       WHERE osm_type = OLD.osm_type and osm_id = OLD.osm_id
359             and class = OLD.class and type = OLD.type;
360   END IF;
361
362   INSERT INTO place_to_be_deleted (osm_type, osm_id, class, type, deferred)
363     VALUES(OLD.osm_type, OLD.osm_id, OLD.class, OLD.type, deferred);
364
365   RETURN NULL;
366 END;
367 $$ LANGUAGE plpgsql;