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