]> git.openstreetmap.org Git - nominatim.git/blob - lib-sql/functions/place_triggers.sql
Switches associatedStreet handling to dedicated table
[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) 2026 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 BEGIN
18   {% if debug %}
19     RAISE WARNING 'place_insert: % % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type,st_area(NEW.geometry);
20   {% endif %}
21
22   -- Filter tuples with bad geometries.
23   IF ST_IsEmpty(NEW.geometry) OR NOT ST_IsValid(NEW.geometry) THEN
24     INSERT INTO import_polygon_error (osm_type, osm_id, class, type, name,
25                                       country_code, updated, errormessage,
26                                       prevgeometry, newgeometry)
27       VALUES (NEW.osm_type, NEW.osm_id, NEW.class, NEW.type, NEW.name,
28               NEW.address->'country', now(), ST_IsValidReason(NEW.geometry),
29               null, NEW.geometry);
30     {% if debug %}
31       RAISE WARNING 'Invalid Geometry: % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type;
32     {% endif %}
33     RETURN null;
34   END IF;
35
36   -- Remove the place from the list of places to be deleted
37   DELETE FROM place_to_be_deleted pdel
38     WHERE pdel.osm_type = NEW.osm_type and pdel.osm_id = NEW.osm_id
39           and pdel.class = NEW.class and pdel.type = NEW.type;
40
41   -- Have we already done this place?
42   SELECT * INTO existing
43     FROM place
44     WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
45           and class = NEW.class and type = NEW.type;
46
47   {% if debug %}RAISE WARNING 'Existing: %',existing.osm_id;{% endif %}
48
49   IF existing.osm_type IS NULL THEN
50     DELETE FROM place where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class;
51   END IF;
52
53   -- Remove any old logged data.
54   DELETE from import_polygon_error where osm_type = NEW.osm_type and osm_id = NEW.osm_id;
55   DELETE from import_polygon_delete where osm_type = NEW.osm_type and osm_id = NEW.osm_id;
56
57   -- ---- All other place types.
58
59   -- When an area is changed from large to small: log and discard change
60   IF existing.geometry is not null AND ST_IsValid(existing.geometry)
61     AND ST_Area(existing.geometry) > 0.02
62     AND ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon')
63     AND ST_Area(NEW.geometry) < ST_Area(existing.geometry) * 0.5
64   THEN
65     INSERT INTO import_polygon_error (osm_type, osm_id, class, type, name,
66                                       country_code, updated, errormessage,
67                                       prevgeometry, newgeometry)
68       VALUES (NEW.osm_type, NEW.osm_id, NEW.class, NEW.type, NEW.name,
69               NEW.address->'country', now(),
70               'Area reduced from '||st_area(existing.geometry)||' to '||st_area(NEW.geometry),
71               existing.geometry, NEW.geometry);
72
73     RETURN null;
74   END IF;
75
76   -- Get the existing placex entry.
77   SELECT * INTO existingplacex
78     FROM placex
79     WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
80           and class = NEW.class and type = NEW.type;
81
82   {% if debug %}RAISE WARNING 'Existing PlaceX: %',existingplacex.place_id;{% endif %}
83
84   -- To paraphrase: if there isn't an existing item, OR if the admin level has changed
85   IF existingplacex.osm_type IS NULL
86      or (existingplacex.class = 'boundary'
87          and ((coalesce(existingplacex.admin_level, 15) != coalesce(NEW.admin_level, 15)
88                and existingplacex.type = 'administrative')
89               or existingplacex.type != NEW.type))
90   THEN
91     {% if config.get_bool('LIMIT_REINDEXING') %}
92     -- sanity check: ignore admin_level changes on places with too many active children
93     -- or we end up reindexing entire countries because somebody accidentally deleted admin_level
94     IF existingplacex.osm_type IS NOT NULL THEN
95       SELECT count(*) INTO i FROM
96         (SELECT 'a' FROM placex, place_addressline
97           WHERE address_place_id = existingplacex.place_id
98                 and placex.place_id = place_addressline.place_id
99                 and indexed_status = 0 and place_addressline.isaddress LIMIT 100001) sub;
100       IF i > 100000 THEN
101         RETURN null;
102       END IF;
103     END IF;
104     {% endif %}
105
106     IF existingplacex.osm_type is not NULL THEN
107       -- Mark any existing place for delete in the placex table
108       UPDATE placex SET indexed_status = 100
109         WHERE placex.osm_type = NEW.osm_type and placex.osm_id = NEW.osm_id
110               and placex.class = NEW.class and placex.type = NEW.type;
111     END IF;
112
113     -- Process it as a new insertion
114     INSERT INTO placex (osm_type, osm_id, class, type, name,
115                         admin_level, address, extratags, geometry)
116       VALUES (NEW.osm_type, NEW.osm_id, NEW.class, NEW.type, NEW.name,
117               NEW.admin_level, NEW.address, NEW.extratags, NEW.geometry);
118
119     {% if debug %}RAISE WARNING 'insert done % % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type,NEW.name;{% endif %}
120
121     IF existing.osm_type is not NULL THEN
122       -- If there is already an entry in place, just update that, if necessary.
123       IF coalesce(existing.name, ''::hstore) != coalesce(NEW.name, ''::hstore)
124          or coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
125          or coalesce(existing.extratags, ''::hstore) != coalesce(NEW.extratags, ''::hstore)
126          or coalesce(existing.admin_level, 15) != coalesce(NEW.admin_level, 15)
127          or existing.geometry::text != NEW.geometry::text
128       THEN
129         UPDATE place
130           SET name = NEW.name,
131               address = NEW.address,
132               extratags = NEW.extratags,
133               admin_level = NEW.admin_level,
134               geometry = NEW.geometry
135           WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
136                 and class = NEW.class and type = NEW.type;
137       END IF;
138
139       RETURN NULL;
140     END IF;
141
142     RETURN NEW;
143   END IF;
144
145   -- Special case for polygon shape changes because they tend to be large
146   -- and we can be a bit clever about how we handle them
147   IF existing.geometry::text != NEW.geometry::text
148      AND ST_GeometryType(existing.geometry) in ('ST_Polygon','ST_MultiPolygon')
149      AND ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon')
150   THEN
151     -- Performance limit
152     IF ST_Area(NEW.geometry) < 0.000000001 AND ST_Area(existingplacex.geometry) < 1
153     THEN
154       -- re-index points that have moved in / out of the polygon.
155       -- Could be done as a single query but postgres gets the index usage wrong.
156       update placex set indexed_status = 2 where indexed_status = 0
157           AND ST_Intersects(NEW.geometry, placex.geometry)
158           AND NOT ST_Intersects(existingplacex.geometry, placex.geometry)
159           AND rank_search > existingplacex.rank_search AND (rank_search < 28 or name is not null);
160
161       update placex set indexed_status = 2 where indexed_status = 0
162           AND ST_Intersects(existingplacex.geometry, placex.geometry)
163           AND NOT ST_Intersects(NEW.geometry, placex.geometry)
164           AND rank_search > existingplacex.rank_search AND (rank_search < 28 or name is not null);
165     END IF;
166   END IF;
167
168
169   -- Has something relevant changed?
170   IF coalesce(existing.name::text, '') != coalesce(NEW.name::text, '')
171      OR coalesce(existing.extratags::text, '') != coalesce(NEW.extratags::text, '')
172      OR coalesce(existing.address, ''::hstore) != coalesce(NEW.address, ''::hstore)
173      OR coalesce(existing.admin_level, 15) != coalesce(NEW.admin_level, 15)
174      OR existing.geometry::text != NEW.geometry::text
175   THEN
176     UPDATE place
177       SET name = NEW.name,
178           address = NEW.address,
179           extratags = NEW.extratags,
180           admin_level = NEW.admin_level,
181           geometry = NEW.geometry
182       WHERE osm_type = NEW.osm_type and osm_id = NEW.osm_id
183             and class = NEW.class and type = NEW.type;
184
185     -- Boundaries must be areas.
186     IF NEW.class in ('boundary')
187        AND ST_GeometryType(NEW.geometry) not in ('ST_Polygon','ST_MultiPolygon')
188     THEN
189       DELETE FROM placex where place_id = existingplacex.place_id;
190       RETURN NULL;
191     END IF;
192
193     -- Update the placex entry in-place.
194     UPDATE placex
195       SET name = NEW.name,
196           address = NEW.address,
197           parent_place_id = null,
198           extratags = NEW.extratags,
199           admin_level = NEW.admin_level,
200           indexed_status = 2,
201           geometry = CASE WHEN existingplacex.rank_address = 0
202                           THEN simplify_large_polygons(NEW.geometry)
203                           ELSE NEW.geometry END
204       WHERE place_id = existingplacex.place_id;
205
206     -- Invalidate linked places: they potentially get a new name and addresses.
207     IF existingplacex.linked_place_id is not NULL THEN
208       UPDATE placex x
209         SET name = p.name,
210             extratags = p.extratags,
211             indexed_status = 2
212         FROM place p
213         WHERE x.place_id = existingplacex.linked_place_id
214               and x.indexed_status = 0
215               and x.osm_type = p.osm_type
216               and x.osm_id = p.osm_id
217               and x.class = p.class;
218     END IF;
219
220     -- Invalidate dependent objects effected by name changes
221     IF coalesce(existing.name::text, '') != coalesce(NEW.name::text, '')
222     THEN
223       IF existingplacex.rank_address between 26 and 27 THEN
224         -- When streets change their name, this may have an effect on POI objects
225         -- with addr:street tags.
226         UPDATE placex SET indexed_status = 2
227           WHERE indexed_status = 0 and address ? 'street'
228                 and parent_place_id = existingplacex.place_id;
229         UPDATE placex SET indexed_status = 2
230           WHERE indexed_status = 0 and rank_search = 30 and address ? 'street'
231                 and ST_DWithin(NEW.geometry, geometry, 0.002);
232       ELSEIF existingplacex.rank_address between 16 and 25 THEN
233         -- When places change their name, this may have an effect on POI objects
234         -- with addr:place tags.
235         UPDATE placex SET indexed_status = 2
236           WHERE indexed_status = 0 and address ? 'place' and rank_search = 30
237                 and parent_place_id = existingplacex.place_id;
238         -- No update of surrounding objects, potentially too expensive.
239       END IF;
240     END IF;
241   END IF;
242
243   -- When an existing way is updated, recalculate entrances
244   IF existingplacex.osm_type = 'W' and (existingplacex.rank_search > 27 or existingplacex.class IN ('landuse', 'leisure')) THEN
245     PERFORM place_update_entrances(existingplacex.place_id, existingplacex.osm_id);
246   END IF;
247
248   -- Abort the insertion (we modified the existing place instead)
249   RETURN NULL;
250 END;
251 $$ LANGUAGE plpgsql;
252
253 CREATE OR REPLACE FUNCTION place_delete()
254   RETURNS TRIGGER
255   AS $$
256 DECLARE
257   deferred BOOLEAN;
258 BEGIN
259   {% if debug %}RAISE WARNING 'Delete for % % %/%', OLD.osm_type, OLD.osm_id, OLD.class, OLD.type;{% endif %}
260
261   deferred := ST_IsValid(OLD.geometry) and ST_Area(OLD.geometry) > 2;
262   IF deferred THEN
263     SELECT bool_or(not (rank_address = 0 or rank_address > 25)) INTO deferred
264       FROM placex
265       WHERE osm_type = OLD.osm_type and osm_id = OLD.osm_id
266             and class = OLD.class and type = OLD.type;
267   END IF;
268
269   INSERT INTO place_to_be_deleted (osm_type, osm_id, class, type, deferred)
270     VALUES(OLD.osm_type, OLD.osm_id, OLD.class, OLD.type, deferred);
271
272   RETURN NULL;
273 END;
274 $$ LANGUAGE plpgsql;