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