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