]> 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   existing RECORD;
14   existingplacex RECORD;
15   existingline RECORD;
16   existinggeometry GEOMETRY;
17   existingplace_id BIGINT;
18   result BOOLEAN;
19   partition INTEGER;
20 BEGIN
21
22   {% if debug %}
23     RAISE WARNING '-----------------------------------------------------------------------------------';
24     RAISE WARNING 'place_insert: % % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type,st_area(NEW.geometry);
25   {% endif %}
26   -- filter wrong tupels
27   IF ST_IsEmpty(NEW.geometry) OR NOT ST_IsValid(NEW.geometry) OR ST_X(ST_Centroid(NEW.geometry))::text in ('NaN','Infinity','-Infinity') OR ST_Y(ST_Centroid(NEW.geometry))::text in ('NaN','Infinity','-Infinity') THEN  
28     INSERT INTO import_polygon_error (osm_type, osm_id, class, type, name, country_code, updated, errormessage, prevgeometry, newgeometry)
29       VALUES (NEW.osm_type, NEW.osm_id, NEW.class, NEW.type, NEW.name, NEW.address->'country', now(), ST_IsValidReason(NEW.geometry), null, NEW.geometry);
30 --    RAISE WARNING 'Invalid Geometry: % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type;
31     RETURN null;
32   END IF;
33
34   -- decide, whether it is an osm interpolation line => insert intoosmline, or else just placex
35   IF NEW.class='place' and NEW.type='houses' and NEW.osm_type='W' and ST_GeometryType(NEW.geometry) = 'ST_LineString' THEN
36     -- Have we already done this place?
37     select * from place where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class and type = NEW.type INTO existing;
38
39     -- Get the existing place_id
40     select * from location_property_osmline where osm_id = NEW.osm_id INTO existingline;
41
42     -- Handle a place changing type by removing the old data (this trigger is executed BEFORE INSERT of the NEW tupel)
43     IF existing.osm_type IS NULL THEN
44       DELETE FROM place where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class;
45     END IF;
46
47     DELETE from import_polygon_error where osm_type = NEW.osm_type and osm_id = NEW.osm_id;
48     DELETE from import_polygon_delete where osm_type = NEW.osm_type and osm_id = NEW.osm_id;
49
50     -- update method for interpolation lines: delete all old interpolation lines with same osm_id (update on place) and insert the new one(s) (they can be split up, if they have > 2 nodes)
51     IF existingline.osm_id IS NOT NULL THEN
52       delete from location_property_osmline where osm_id = NEW.osm_id;
53     END IF;
54
55     -- for interpolations invalidate all nodes on the line
56     update placex p set indexed_status = 2
57       from planet_osm_ways w
58       where w.id = NEW.osm_id and p.osm_type = 'N' and p.osm_id = any(w.nodes);
59
60
61     INSERT INTO location_property_osmline (osm_id, address, linegeo)
62       VALUES (NEW.osm_id, NEW.address, NEW.geometry);
63
64
65     IF existing.osm_type IS NULL THEN
66       return NEW;
67     END IF;
68
69     IF coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
70        OR (coalesce(existing.extratags, ''::hstore) != coalesce(NEW.extratags, ''::hstore))
71        OR existing.geometry::text != NEW.geometry::text
72        THEN
73
74       update place set 
75         name = NEW.name,
76         address = NEW.address,
77         extratags = NEW.extratags,
78         admin_level = NEW.admin_level,
79         geometry = NEW.geometry
80         where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class and type = NEW.type;
81     END IF;
82
83     RETURN NULL;
84
85   ELSE -- insert to placex
86
87     -- Patch in additional country names
88     IF NEW.admin_level = 2 AND NEW.type = 'administrative'
89           AND NEW.address is not NULL AND NEW.address ? 'country' THEN
90         SELECT name FROM country_name WHERE country_code = lower(NEW.address->'country') INTO existing;
91         IF existing.name IS NOT NULL THEN
92             NEW.name = existing.name || NEW.name;
93         END IF;
94     END IF;
95       
96     -- Have we already done this place?
97     select * from place where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class and type = NEW.type INTO existing;
98
99     -- Get the existing place_id
100     select * from placex where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class and type = NEW.type INTO existingplacex;
101
102     -- Handle a place changing type by removing the old data
103     -- My generated 'place' types are causing havok because they overlap with real keys
104     -- TODO: move them to their own special purpose key/class to avoid collisions
105     IF existing.osm_type IS NULL THEN
106       DELETE FROM place where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class;
107     END IF;
108
109     -- Pure postcodes are never queried from placex so we don't add them.
110     -- location_postcodes is filled from the place table directly.
111     IF NEW.class = 'place' AND NEW.type = 'postcode' THEN
112       -- Remove old placex entry.
113       DELETE FROM placex where osm_type = NEW.osm_type and osm_id = NEW.osm_id;
114
115       IF existing.osm_type IS NOT NULL THEN
116         IF coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
117            OR existing.geometry::text != NEW.geometry::text
118         THEN
119
120           update place set address = NEW.address, geometry = NEW.geometry
121             where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class and type = NEW.type;
122         END IF;
123
124         RETURN NULL;
125       END IF;
126
127       RETURN NEW;
128     END IF;
129
130     {% if debug %}RAISE WARNING 'Existing: %',existing.osm_id;{% endif %}
131     {% if debug %}RAISE WARNING 'Existing PlaceX: %',existingplacex.place_id;{% endif %}
132
133     -- Log and discard 
134     IF existing.geometry is not null AND st_isvalid(existing.geometry) 
135       AND st_area(existing.geometry) > 0.02
136       AND ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon')
137       AND st_area(NEW.geometry) < st_area(existing.geometry)*0.5
138       THEN
139       INSERT INTO import_polygon_error (osm_type, osm_id, class, type, name, country_code, updated, errormessage, prevgeometry, newgeometry)
140         VALUES (NEW.osm_type, NEW.osm_id, NEW.class, NEW.type, NEW.name, NEW.address->'country', now(), 
141         'Area reduced from '||st_area(existing.geometry)||' to '||st_area(NEW.geometry), existing.geometry, NEW.geometry);
142       RETURN null;
143     END IF;
144
145     DELETE from import_polygon_error where osm_type = NEW.osm_type and osm_id = NEW.osm_id;
146     DELETE from import_polygon_delete where osm_type = NEW.osm_type and osm_id = NEW.osm_id;
147
148     -- To paraphrase, if there isn't an existing item, OR if the admin level has changed
149     IF existingplacex.osm_type IS NULL OR
150         (existingplacex.class = 'boundary' AND
151           ((coalesce(existingplacex.admin_level, 15) != coalesce(NEW.admin_level, 15) AND existingplacex.type = 'administrative') OR
152           (existingplacex.type != NEW.type)))
153     THEN
154
155       {% if config.get_bool('LIMIT_REINDEXING') %}
156       IF existingplacex.osm_type IS NOT NULL THEN
157         -- sanity check: ignore admin_level changes on places with too many active children
158         -- or we end up reindexing entire countries because somebody accidentally deleted admin_level
159         SELECT count(*) INTO i FROM
160           (SELECT 'a' FROM placex, place_addressline 
161             WHERE address_place_id = existingplacex.place_id
162                   and placex.place_id = place_addressline.place_id
163                   and indexed_status = 0 and place_addressline.isaddress LIMIT 100001) sub;
164         IF i > 100000 THEN
165           RETURN null;
166         END IF;
167       END IF;
168       {% endif %}
169
170       IF existing.osm_type IS NOT NULL THEN
171         -- pathological case caused by the triggerless copy into place during initial import
172         -- force delete even for large areas, it will be reinserted later
173         UPDATE place set geometry = ST_SetSRID(ST_Point(0,0), 4326) where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class and type = NEW.type;
174         DELETE from place where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class and type = NEW.type;
175       END IF;
176
177       -- No - process it as a new insertion (hopefully of low rank or it will be slow)
178       insert into placex (osm_type, osm_id, class, type, name,
179                           admin_level, address, extratags, geometry)
180         values (NEW.osm_type, NEW.osm_id, NEW.class, NEW.type, NEW.name,
181                 NEW.admin_level, NEW.address, NEW.extratags, NEW.geometry);
182
183       {% if debug %}RAISE WARNING 'insert done % % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type,NEW.name;{% endif %}
184
185       RETURN NEW;
186     END IF;
187
188     -- Special case for polygon shape changes because they tend to be large and we can be a bit clever about how we handle them
189     IF existing.geometry::text != NEW.geometry::text 
190        AND ST_GeometryType(existing.geometry) in ('ST_Polygon','ST_MultiPolygon')
191        AND ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon') 
192        THEN 
193
194       -- Get the version of the geometry actually used (in placex table)
195       select geometry from placex where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class and type = NEW.type into existinggeometry;
196
197       -- Performance limit
198       IF st_area(NEW.geometry) < 0.000000001 AND st_area(existinggeometry) < 1 THEN
199
200         -- re-index points that have moved in / out of the polygon, could be done as a single query but postgres gets the index usage wrong
201         update placex set indexed_status = 2 where indexed_status = 0
202             AND ST_Intersects(NEW.geometry, placex.geometry)
203             AND NOT ST_Intersects(existinggeometry, placex.geometry)
204             AND rank_search > existingplacex.rank_search AND (rank_search < 28 or name is not null);
205
206         update placex set indexed_status = 2 where indexed_status = 0
207             AND ST_Intersects(existinggeometry, placex.geometry)
208             AND NOT ST_Intersects(NEW.geometry, placex.geometry)
209             AND rank_search > existingplacex.rank_search AND (rank_search < 28 or name is not null);
210
211       END IF;
212
213     END IF;
214
215
216     IF coalesce(existing.name::text, '') != coalesce(NEW.name::text, '')
217        OR coalesce(existing.extratags::text, '') != coalesce(NEW.extratags::text, '')
218        OR coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
219        OR coalesce(existing.admin_level, 15) != coalesce(NEW.admin_level, 15)
220        OR existing.geometry::text != NEW.geometry::text
221        THEN
222
223       update place set 
224         name = NEW.name,
225         address = NEW.address,
226         extratags = NEW.extratags,
227         admin_level = NEW.admin_level,
228         geometry = NEW.geometry
229         where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class and type = NEW.type;
230
231
232       IF NEW.class = 'boundary' AND NEW.type = 'postal_code' THEN
233           IF NEW.address is NULL OR NOT NEW.address ? 'postcode' THEN
234               -- postcode was deleted, no longer retain in placex
235               DELETE FROM placex where place_id = existingplacex.place_id;
236               RETURN NULL;
237           END IF;
238
239           NEW.name := hstore('ref', NEW.address->'postcode');
240       END IF;
241
242       IF NEW.class in ('boundary')
243          AND ST_GeometryType(NEW.geometry) not in ('ST_Polygon','ST_MultiPolygon') THEN
244           DELETE FROM placex where place_id = existingplacex.place_id;
245           RETURN NULL;
246       END IF;
247
248       update placex set 
249         name = NEW.name,
250         address = NEW.address,
251         parent_place_id = null,
252         extratags = NEW.extratags,
253         admin_level = NEW.admin_level,
254         indexed_status = 2,
255         geometry = NEW.geometry
256         where place_id = existingplacex.place_id;
257
258       -- if a node(=>house), which is part of a interpolation line, changes (e.g. the street attribute) => mark this line for reparenting 
259       -- (already here, because interpolation lines are reindexed before nodes, so in the second call it would be too late)
260       IF NEW.osm_type='N'
261          and (coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
262              or existing.geometry::text != NEW.geometry::text)
263       THEN
264           result:= osmline_reinsert(NEW.osm_id, NEW.geometry);
265       END IF;
266
267       -- linked places should get potential new naming and addresses
268       IF existingplacex.linked_place_id is not NULL THEN
269         update placex x set
270           name = p.name,
271           extratags = p.extratags,
272           indexed_status = 2
273         from place p
274         where x.place_id = existingplacex.linked_place_id
275               and x.indexed_status = 0
276               and x.osm_type = p.osm_type
277               and x.osm_id = p.osm_id
278               and x.class = p.class;
279       END IF;
280
281       IF coalesce(existing.name::text, '') != coalesce(NEW.name::text, '')
282       THEN
283         IF existingplacex.rank_address between 26 and 27 THEN
284           -- When streets change their name, this may have an effect on POI objects
285           -- with addr:street tags.
286           UPDATE placex SET indexed_status = 2
287           WHERE indexed_status = 0 and address ? 'street'
288                 and parent_place_id = existingplacex.place_id;
289           UPDATE placex SET indexed_status = 2
290           WHERE indexed_status = 0 and rank_search = 30 and address ? 'street'
291                 and ST_DWithin(NEW.geometry, geometry, 0.002);
292         ELSEIF existingplacex.rank_address between 16 and 25 THEN
293           -- When places change their name, this may have an effect on POI objects
294           -- with addr:place tags.
295           UPDATE placex SET indexed_status = 2
296           WHERE indexed_status = 0 and address ? 'place' and rank_search = 30
297                 and parent_place_id = existingplacex.place_id;
298           -- No update of surrounding objects, potentially too expensive.
299         END IF;
300       END IF;
301     END IF;
302
303     -- Abort the add (we modified the existing place instead)
304     RETURN NULL;
305   END IF;
306
307 END;
308 $$ LANGUAGE plpgsql;
309
310
311 CREATE OR REPLACE FUNCTION place_delete()
312   RETURNS TRIGGER
313   AS $$
314 DECLARE
315   has_rank BOOLEAN;
316 BEGIN
317
318   {% if debug %}RAISE WARNING 'delete: % % % %',OLD.osm_type,OLD.osm_id,OLD.class,OLD.type;{% endif %}
319
320   -- deleting large polygons can have a massive effect on the system - require manual intervention to let them through
321   IF st_area(OLD.geometry) > 2 and st_isvalid(OLD.geometry) THEN
322     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;
323     IF has_rank THEN
324       insert into import_polygon_delete (osm_type, osm_id, class, type) values (OLD.osm_type,OLD.osm_id,OLD.class,OLD.type);
325       RETURN NULL;
326     END IF;
327   END IF;
328
329   -- mark for delete
330   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;
331
332   -- interpolations are special
333   IF OLD.osm_type='W' and OLD.class = 'place' and OLD.type = 'houses' THEN
334     UPDATE location_property_osmline set indexed_status = 100 where osm_id = OLD.osm_id; -- osm_id = wayid (=old.osm_id)
335   END IF;
336
337   RETURN OLD;
338 END;
339 $$
340 LANGUAGE plpgsql;
341