]> git.openstreetmap.org Git - nominatim.git/blob - sql/functions.sql
f28e1d0ff9540ded4c92f4caf7efef1c4eb2f64b
[nominatim.git] / sql / functions.sql
1 --DROP TRIGGER IF EXISTS place_before_insert on placex;
2 --DROP TRIGGER IF EXISTS place_before_update on placex;
3 --CREATE TYPE addresscalculationtype AS (
4 --  word text,
5 --  score integer
6 --);
7
8
9 CREATE OR REPLACE FUNCTION getclasstypekey(c text, t text) RETURNS TEXT
10   AS $$
11 DECLARE
12 BEGIN
13   RETURN c||'|'||t;
14 END;
15 $$
16 LANGUAGE plpgsql IMMUTABLE;
17
18 CREATE OR REPLACE FUNCTION isbrokengeometry(place geometry) RETURNS BOOLEAN
19   AS $$
20 DECLARE
21   NEWgeometry geometry;
22 BEGIN
23   NEWgeometry := place;
24   IF ST_IsEmpty(NEWgeometry) OR NOT ST_IsValid(NEWgeometry) OR ST_X(ST_Centroid(NEWgeometry))::text in ('NaN','Infinity','-Infinity') OR ST_Y(ST_Centroid(NEWgeometry))::text in ('NaN','Infinity','-Infinity') THEN  
25     RETURN true;
26   END IF;
27   RETURN false;
28 END;
29 $$
30 LANGUAGE plpgsql IMMUTABLE;
31
32 CREATE OR REPLACE FUNCTION clean_geometry(place geometry) RETURNS geometry
33   AS $$
34 DECLARE
35   NEWgeometry geometry;
36 BEGIN
37   NEWgeometry := place;
38   IF ST_X(ST_Centroid(NEWgeometry))::text in ('NaN','Infinity','-Infinity') OR ST_Y(ST_Centroid(NEWgeometry))::text in ('NaN','Infinity','-Infinity') THEN  
39     NEWgeometry := ST_buffer(NEWgeometry,0);
40     IF ST_X(ST_Centroid(NEWgeometry))::text in ('NaN','Infinity','-Infinity') OR ST_Y(ST_Centroid(NEWgeometry))::text in ('NaN','Infinity','-Infinity') THEN  
41       RETURN ST_SetSRID(ST_Point(0,0),4326);
42     END IF;
43   END IF;
44   RETURN NEWgeometry;
45 END;
46 $$
47 LANGUAGE plpgsql IMMUTABLE;
48
49 CREATE OR REPLACE FUNCTION geometry_sector(place geometry) RETURNS INTEGER
50   AS $$
51 DECLARE
52   NEWgeometry geometry;
53 BEGIN
54 --  RAISE WARNING '%',place;
55   NEWgeometry := place;
56   IF ST_IsEmpty(NEWgeometry) OR NOT ST_IsValid(NEWgeometry) OR ST_X(ST_Centroid(NEWgeometry))::text in ('NaN','Infinity','-Infinity') OR ST_Y(ST_Centroid(NEWgeometry))::text in ('NaN','Infinity','-Infinity') THEN  
57     NEWgeometry := ST_buffer(NEWgeometry,0);
58     IF ST_IsEmpty(NEWgeometry) OR NOT ST_IsValid(NEWgeometry) OR ST_X(ST_Centroid(NEWgeometry))::text in ('NaN','Infinity','-Infinity') OR ST_Y(ST_Centroid(NEWgeometry))::text in ('NaN','Infinity','-Infinity') THEN  
59       RETURN 0;
60     END IF;
61   END IF;
62   RETURN (500-ST_X(ST_Centroid(NEWgeometry))::integer)*1000 + (500-ST_Y(ST_Centroid(NEWgeometry))::integer);
63 END;
64 $$
65 LANGUAGE plpgsql IMMUTABLE;
66
67 CREATE OR REPLACE FUNCTION debug_geometry_sector(osmid integer, place geometry) RETURNS INTEGER
68   AS $$
69 DECLARE
70   NEWgeometry geometry;
71 BEGIN
72 --  RAISE WARNING '%',osmid;
73   IF osmid = 61315 THEN
74     return null;
75   END IF;
76   NEWgeometry := place;
77   IF ST_IsEmpty(NEWgeometry) OR NOT ST_IsValid(NEWgeometry) OR ST_X(ST_Centroid(NEWgeometry))::text in ('NaN','Infinity','-Infinity') OR ST_Y(ST_Centroid(NEWgeometry))::text in ('NaN','Infinity','-Infinity') THEN  
78     NEWgeometry := ST_buffer(NEWgeometry,0);
79     IF ST_IsEmpty(NEWgeometry) OR NOT ST_IsValid(NEWgeometry) OR ST_X(ST_Centroid(NEWgeometry))::text in ('NaN','Infinity','-Infinity') OR ST_Y(ST_Centroid(NEWgeometry))::text in ('NaN','Infinity','-Infinity') THEN  
80       RETURN NULL;
81     END IF;
82   END IF;
83   RETURN (500-ST_X(ST_Centroid(NEWgeometry))::integer)*1000 + (500-ST_Y(ST_Centroid(NEWgeometry))::integer);
84 END;
85 $$
86 LANGUAGE plpgsql IMMUTABLE;
87
88 CREATE OR REPLACE FUNCTION geometry_index(place geometry, indexed BOOLEAN, name HSTORE) RETURNS INTEGER
89   AS $$
90 BEGIN
91 IF indexed THEN RETURN NULL; END IF;
92 IF name is null THEN RETURN NULL; END IF;
93 RETURN geometry_sector(place);
94 END;
95 $$
96 LANGUAGE plpgsql IMMUTABLE;
97
98 CREATE OR REPLACE FUNCTION geometry_index(sector integer, indexed BOOLEAN, name HSTORE) RETURNS INTEGER
99   AS $$
100 BEGIN
101 IF indexed THEN RETURN NULL; END IF;
102 IF name is null THEN RETURN NULL; END IF;
103 RETURN sector;
104 END;
105 $$
106 LANGUAGE plpgsql IMMUTABLE;
107
108 CREATE OR REPLACE FUNCTION transliteration(text) RETURNS text
109   AS '{modulepath}/nominatim.so', 'transliteration'
110 LANGUAGE c IMMUTABLE STRICT;
111
112 CREATE OR REPLACE FUNCTION gettokenstring(text) RETURNS text
113   AS '{modulepath}/nominatim.so', 'gettokenstring'
114 LANGUAGE c IMMUTABLE STRICT;
115
116 CREATE OR REPLACE FUNCTION make_standard_name(name TEXT) RETURNS TEXT
117   AS $$
118 DECLARE
119   o TEXT;
120 BEGIN
121   o := gettokenstring(transliteration(name));
122   RETURN trim(substr(o,1,length(o)));
123 END;
124 $$
125 LANGUAGE 'plpgsql' IMMUTABLE;
126
127 CREATE OR REPLACE FUNCTION getorcreate_word_id(lookup_word TEXT) 
128   RETURNS INTEGER
129   AS $$
130 DECLARE
131   lookup_token TEXT;
132   return_word_id INTEGER;
133 BEGIN
134   lookup_token := trim(lookup_word);
135   SELECT min(word_id) FROM word WHERE word_token = lookup_token and class is null and type is null into return_word_id;
136   IF return_word_id IS NULL THEN
137     return_word_id := nextval('seq_word');
138     INSERT INTO word VALUES (return_word_id, lookup_token, regexp_replace(lookup_token,E'([^0-9])\\1+',E'\\1','g'), null, null, null, null, 0, null);
139   END IF;
140   RETURN return_word_id;
141 END;
142 $$
143 LANGUAGE plpgsql;
144
145 CREATE OR REPLACE FUNCTION getorcreate_housenumber_id(lookup_word TEXT)
146   RETURNS INTEGER
147   AS $$
148 DECLARE
149   lookup_token TEXT;
150   return_word_id INTEGER;
151 BEGIN
152   lookup_token := ' '||trim(lookup_word);
153   SELECT min(word_id) FROM word WHERE word_token = lookup_token and class='place' and type='house' into return_word_id;
154   IF return_word_id IS NULL THEN
155     return_word_id := nextval('seq_word');
156     INSERT INTO word VALUES (return_word_id, lookup_token, null, null, 'place', 'house', null, 0, null);
157   END IF;
158   RETURN return_word_id;
159 END;
160 $$
161 LANGUAGE plpgsql;
162
163 CREATE OR REPLACE FUNCTION getorcreate_country(lookup_word TEXT, lookup_country_code varchar(2))
164   RETURNS INTEGER
165   AS $$
166 DECLARE
167   lookup_token TEXT;
168   return_word_id INTEGER;
169 BEGIN
170   lookup_token := ' '||trim(lookup_word);
171   SELECT min(word_id) FROM word WHERE word_token = lookup_token and country_code=lookup_country_code into return_word_id;
172   IF return_word_id IS NULL THEN
173     return_word_id := nextval('seq_word');
174     INSERT INTO word VALUES (return_word_id, lookup_token, null, null, null, null, lookup_country_code, 0, null);
175   END IF;
176   RETURN return_word_id;
177 END;
178 $$
179 LANGUAGE plpgsql;
180
181 CREATE OR REPLACE FUNCTION getorcreate_amenity(lookup_word TEXT, lookup_class text, lookup_type text)
182   RETURNS INTEGER
183   AS $$
184 DECLARE
185   lookup_token TEXT;
186   return_word_id INTEGER;
187 BEGIN
188   lookup_token := ' '||trim(lookup_word);
189   SELECT min(word_id) FROM word WHERE word_token = lookup_token and class=lookup_class and type = lookup_type into return_word_id;
190   IF return_word_id IS NULL THEN
191     return_word_id := nextval('seq_word');
192     INSERT INTO word VALUES (return_word_id, lookup_token, null, null, lookup_class, lookup_type, null, 0, null);
193   END IF;
194   RETURN return_word_id;
195 END;
196 $$
197 LANGUAGE plpgsql;
198
199 CREATE OR REPLACE FUNCTION getorcreate_tagpair(lookup_class text, lookup_type text)
200   RETURNS INTEGER
201   AS $$
202 DECLARE
203   lookup_token TEXT;
204   return_word_id INTEGER;
205 BEGIN
206   lookup_token := lookup_class||'='||lookup_type;
207   SELECT min(word_id) FROM word WHERE word_token = lookup_token into return_word_id;
208   IF return_word_id IS NULL THEN
209     return_word_id := nextval('seq_word');
210     INSERT INTO word VALUES (return_word_id, lookup_token, null, null, null, null, null, 0, null);
211   END IF;
212   RETURN return_word_id;
213 END;
214 $$
215 LANGUAGE plpgsql;
216
217 CREATE OR REPLACE FUNCTION get_tagpair(lookup_class text, lookup_type text)
218   RETURNS INTEGER
219   AS $$
220 DECLARE
221   lookup_token TEXT;
222   return_word_id INTEGER;
223 BEGIN
224   lookup_token := lookup_class||'='||lookup_type;
225   SELECT min(word_id) FROM word WHERE word_token = lookup_token into return_word_id;
226   RETURN return_word_id;
227 END;
228 $$
229 LANGUAGE plpgsql;
230
231 CREATE OR REPLACE FUNCTION getorcreate_amenityoperator(lookup_word TEXT, lookup_class text, lookup_type text, op text)
232   RETURNS INTEGER
233   AS $$
234 DECLARE
235   lookup_token TEXT;
236   return_word_id INTEGER;
237 BEGIN
238   lookup_token := ' '||trim(lookup_word);
239   SELECT min(word_id) FROM word WHERE word_token = lookup_token and class=lookup_class and type = lookup_type and operator = op into return_word_id;
240   IF return_word_id IS NULL THEN
241     return_word_id := nextval('seq_word');
242     INSERT INTO word VALUES (return_word_id, lookup_token, null, null, lookup_class, lookup_type, null, 0, null, op);
243   END IF;
244   RETURN return_word_id;
245 END;
246 $$
247 LANGUAGE plpgsql;
248
249 CREATE OR REPLACE FUNCTION getorcreate_name_id(lookup_word TEXT, src_word TEXT) 
250   RETURNS INTEGER
251   AS $$
252 DECLARE
253   lookup_token TEXT;
254   nospace_lookup_token TEXT;
255   return_word_id INTEGER;
256 BEGIN
257   lookup_token := ' '||trim(lookup_word);
258   SELECT min(word_id) FROM word WHERE word_token = lookup_token and class is null and type is null into return_word_id;
259   IF return_word_id IS NULL THEN
260     return_word_id := nextval('seq_word');
261     INSERT INTO word VALUES (return_word_id, lookup_token, regexp_replace(lookup_token,E'([^0-9])\\1+',E'\\1','g'), src_word, null, null, null, 0, null);
262 --    nospace_lookup_token := replace(replace(lookup_token, '-',''), ' ','');
263 --    IF ' '||nospace_lookup_token != lookup_token THEN
264 --      INSERT INTO word VALUES (return_word_id, '-'||nospace_lookup_token, null, src_word, null, null, null, 0, null);
265 --    END IF;
266   END IF;
267   RETURN return_word_id;
268 END;
269 $$
270 LANGUAGE plpgsql;
271
272 CREATE OR REPLACE FUNCTION getorcreate_name_id(lookup_word TEXT) 
273   RETURNS INTEGER
274   AS $$
275 DECLARE
276 BEGIN
277   RETURN getorcreate_name_id(lookup_word, '');
278 END;
279 $$
280 LANGUAGE plpgsql;
281
282 CREATE OR REPLACE FUNCTION get_word_id(lookup_word TEXT) 
283   RETURNS INTEGER
284   AS $$
285 DECLARE
286   lookup_token TEXT;
287   return_word_id INTEGER;
288 BEGIN
289   lookup_token := trim(lookup_word);
290   SELECT min(word_id) FROM word WHERE word_token = lookup_token and class is null and type is null into return_word_id;
291   RETURN return_word_id;
292 END;
293 $$
294 LANGUAGE plpgsql IMMUTABLE;
295
296 CREATE OR REPLACE FUNCTION get_name_id(lookup_word TEXT) 
297   RETURNS INTEGER
298   AS $$
299 DECLARE
300   lookup_token TEXT;
301   return_word_id INTEGER;
302 BEGIN
303   lookup_token := ' '||trim(lookup_word);
304   SELECT min(word_id) FROM word WHERE word_token = lookup_token and class is null and type is null into return_word_id;
305   RETURN return_word_id;
306 END;
307 $$
308 LANGUAGE plpgsql IMMUTABLE;
309
310 CREATE OR REPLACE FUNCTION array_merge(a INTEGER[], b INTEGER[])
311   RETURNS INTEGER[]
312   AS $$
313 DECLARE
314   i INTEGER;
315   r INTEGER[];
316 BEGIN
317   IF array_upper(a, 1) IS NULL THEN
318     RETURN b;
319   END IF;
320   IF array_upper(b, 1) IS NULL THEN
321     RETURN a;
322   END IF;
323   r := a;
324   FOR i IN 1..array_upper(b, 1) LOOP  
325     IF NOT (ARRAY[b[i]] && r) THEN
326       r := r || b[i];
327     END IF;
328   END LOOP;
329   RETURN r;
330 END;
331 $$
332 LANGUAGE plpgsql IMMUTABLE;
333
334 CREATE OR REPLACE FUNCTION make_keywords(src HSTORE) RETURNS INTEGER[]
335   AS $$
336 DECLARE
337   result INTEGER[];
338   s TEXT;
339   w INTEGER;
340   words TEXT[];
341   item RECORD;
342   j INTEGER;
343 BEGIN
344   result := '{}'::INTEGER[];
345
346   FOR item IN SELECT (each(src)).* LOOP
347
348     s := make_standard_name(item.value);
349
350     w := getorcreate_name_id(s, item.value);
351     result := result | w;
352
353     words := string_to_array(s, ' ');
354     IF array_upper(words, 1) IS NOT NULL THEN
355       FOR j IN 1..array_upper(words, 1) LOOP
356         IF (words[j] != '') THEN
357           w = getorcreate_word_id(words[j]);
358           IF NOT (ARRAY[w] && result) THEN
359             result := result | w;
360           END IF;
361         END IF;
362       END LOOP;
363     END IF;
364
365     words := regexp_split_to_array(item.value, E'[,;()]');
366     IF array_upper(words, 1) != 1 THEN
367       FOR j IN 1..array_upper(words, 1) LOOP
368         s := make_standard_name(words[j]);
369         IF s != '' THEN
370           w := getorcreate_word_id(s);
371           IF NOT (ARRAY[w] && result) THEN
372             result := result | w;
373           END IF;
374         END IF;
375       END LOOP;
376     END IF;
377
378     s := regexp_replace(item.value, '市$', '');
379     IF s != item.value THEN
380       s := make_standard_name(s);
381       IF s != '' THEN
382         w := getorcreate_name_id(s, item.value);
383         IF NOT (ARRAY[w] && result) THEN
384           result := result | w;
385         END IF;
386       END IF;
387     END IF;
388
389   END LOOP;
390
391   RETURN result;
392 END;
393 $$
394 LANGUAGE plpgsql IMMUTABLE;
395
396 CREATE OR REPLACE FUNCTION make_keywords(src TEXT) RETURNS INTEGER[]
397   AS $$
398 DECLARE
399   result INTEGER[];
400   s TEXT;
401   w INTEGER;
402   words TEXT[];
403   i INTEGER;
404   j INTEGER;
405 BEGIN
406   result := '{}'::INTEGER[];
407
408   s := make_standard_name(src);
409   w := getorcreate_name_id(s);
410
411   IF NOT (ARRAY[w] && result) THEN
412     result := result || w;
413   END IF;
414
415   words := string_to_array(s, ' ');
416   IF array_upper(words, 1) IS NOT NULL THEN
417     FOR j IN 1..array_upper(words, 1) LOOP
418       IF (words[j] != '') THEN
419         w = getorcreate_word_id(words[j]);
420         IF NOT (ARRAY[w] && result) THEN
421           result := result || w;
422         END IF;
423       END IF;
424     END LOOP;
425   END IF;
426
427   RETURN result;
428 END;
429 $$
430 LANGUAGE plpgsql IMMUTABLE;
431
432 CREATE OR REPLACE FUNCTION get_word_score(wordscores wordscore[], words text[]) RETURNS integer
433   AS $$
434 DECLARE
435   idxword integer;
436   idxscores integer;
437   result integer;
438 BEGIN
439   IF (wordscores is null OR words is null) THEN
440     RETURN 0;
441   END IF;
442
443   result := 0;
444   FOR idxword in 1 .. array_upper(words, 1) LOOP
445     FOR idxscores in 1 .. array_upper(wordscores, 1) LOOP
446       IF wordscores[idxscores].word = words[idxword] THEN
447         result := result + wordscores[idxscores].score;
448       END IF;
449     END LOOP;
450   END LOOP;
451
452   RETURN result;
453 END;
454 $$
455 LANGUAGE plpgsql IMMUTABLE;
456
457 CREATE OR REPLACE FUNCTION get_country_code(place geometry) RETURNS TEXT
458   AS $$
459 DECLARE
460   place_centre GEOMETRY;
461   nearcountry RECORD;
462 BEGIN
463   place_centre := ST_Centroid(place);
464
465 --RAISE WARNING 'start: %', ST_AsText(place_centre);
466
467   -- Try for a OSM polygon first
468   FOR nearcountry IN select country_code from location_area_country where country_code is not null and st_contains(geometry, place_centre) limit 1
469   LOOP
470     RETURN nearcountry.country_code;
471   END LOOP;
472
473 --RAISE WARNING 'osm fallback: %', ST_AsText(place_centre);
474
475   -- Try for OSM fallback data
476   FOR nearcountry IN select country_code from country_osm_grid where st_contains(geometry, place_centre) limit 1
477   LOOP
478     RETURN nearcountry.country_code;
479   END LOOP;
480
481 --RAISE WARNING 'natural earth: %', ST_AsText(place_centre);
482
483   -- Natural earth data (first fallback)
484 --  FOR nearcountry IN select country_code from country_naturalearthdata where st_contains(geometry, place_centre) limit 1
485 --  LOOP
486 --    RETURN nearcountry.country_code;
487 --  END LOOP;
488
489 --RAISE WARNING 'in country: %', ST_AsText(place_centre);
490
491   -- WorldBoundaries data (second fallback - think there might be something broken in this data)
492   FOR nearcountry IN select country_code from country where st_contains(geometry, place_centre) limit 1
493   LOOP
494     RETURN nearcountry.country_code;
495   END LOOP;
496
497 --RAISE WARNING 'near country: %', ST_AsText(place_centre);
498
499   -- Still not in a country - try nearest within ~12 miles of a country
500   FOR nearcountry IN select country_code from country where st_distance(geometry, place_centre) < 0.5 
501     order by st_distance(geometry, place) limit 1
502   LOOP
503     RETURN nearcountry.country_code;
504   END LOOP;
505
506   RETURN NULL;
507 END;
508 $$
509 LANGUAGE plpgsql IMMUTABLE;
510
511 CREATE OR REPLACE FUNCTION get_country_language_code(search_country_code VARCHAR(2)) RETURNS TEXT
512   AS $$
513 DECLARE
514   nearcountry RECORD;
515 BEGIN
516   FOR nearcountry IN select distinct country_default_language_code from country where country_code = search_country_code limit 1
517   LOOP
518     RETURN lower(nearcountry.country_default_language_code);
519   END LOOP;
520   RETURN NULL;
521 END;
522 $$
523 LANGUAGE plpgsql IMMUTABLE;
524
525 CREATE OR REPLACE FUNCTION delete_location(OLD_place_id INTEGER) RETURNS BOOLEAN
526   AS $$
527 DECLARE
528 BEGIN
529   DELETE FROM location_area where place_id = OLD_place_id;
530 -- TODO:location_area
531   RETURN true;
532 END;
533 $$
534 LANGUAGE plpgsql;
535
536 CREATE OR REPLACE FUNCTION add_location(
537     place_id INTEGER,
538     place_country_code varchar(2),
539     name hstore,
540     rank_search INTEGER,
541     rank_address INTEGER,
542     geometry GEOMETRY
543   ) 
544   RETURNS BOOLEAN
545   AS $$
546 DECLARE
547   keywords INTEGER[];
548   country_code VARCHAR(2);
549   partition VARCHAR(10);
550   locationid INTEGER;
551   isarea BOOLEAN;
552   xmin INTEGER;
553   ymin INTEGER;
554   xmax INTEGER;
555   ymax INTEGER;
556   lon INTEGER;
557   lat INTEGER;
558   centroid GEOMETRY;
559   secgeo GEOMETRY;
560   diameter FLOAT;
561   x BOOLEAN;
562 BEGIN
563
564   -- Allocate all tokens ids - prevents multi-processor race condition later on at cost of slowing down import
565   keywords := make_keywords(name);
566
567   -- 26 = street/highway
568   IF rank_search <= 26 THEN
569     IF place_country_code IS NULL THEN
570       country_code := get_country_code(geometry);
571     END IF;
572     country_code := lower(place_country_code);
573     partition := country_code;
574     IF partition is null THEN
575       partition := 'none';
576     END IF;
577
578     isarea := false;
579     IF (ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon') AND ST_IsValid(geometry)) THEN
580
581       isArea := true;
582       centroid := ST_Centroid(geometry);
583
584       xmin := floor(st_xmin(geometry));
585       xmax := ceil(st_xmax(geometry));
586       ymin := floor(st_ymin(geometry));
587       ymax := ceil(st_ymax(geometry));
588
589       IF xmin = xmax OR ymin = ymax OR (xmax-xmin < 2 AND ymax-ymin < 2) THEN
590         x := insertLocationAreaLarge(partition, place_id, country_code, keywords, rank_search, rank_address, false, centroid, geometry);
591       ELSE
592         FOR lon IN xmin..(xmax-1) LOOP
593           FOR lat IN ymin..(ymax-1) LOOP
594             secgeo := st_intersection(geometry, ST_SetSRID(ST_MakeBox2D(ST_Point(lon,lat),ST_Point(lon+1,lat+1)),4326));
595             IF NOT ST_IsEmpty(secgeo) AND ST_GeometryType(secgeo) in ('ST_Polygon','ST_MultiPolygon') THEN
596               x := insertLocationAreaLarge(partition, place_id, country_code, keywords, rank_search, rank_address, false, centroid, secgeo);
597             END IF;
598           END LOOP;
599         END LOOP;
600       END IF;
601
602     ELSEIF rank_search < 26 THEN
603
604       diameter := 0.02;
605       IF rank_search = 14 THEN
606         diameter := 1;
607       ELSEIF rank_search = 15 THEN
608         diameter := 0.5;
609       ELSEIF rank_search = 16 THEN
610         diameter := 0.15;
611       ELSEIF rank_search = 17 THEN
612         diameter := 0.05;
613       ELSEIF rank_search = 25 THEN
614         diameter := 0.005;
615       END IF;
616
617       secgeo := ST_Buffer(geometry, diameter);
618       x := insertLocationAreaLarge(partition, place_id, country_code, keywords, rank_search, rank_address, false, ST_Centroid(geometry), secgeo);
619
620     ELSE
621
622       -- ~ 20meters
623       secgeo := ST_Buffer(geometry, 0.0002);
624       x := insertLocationAreaRoadNear(partition, place_id, country_code, keywords, rank_search, rank_address, false, ST_Centroid(geometry), secgeo);
625
626       -- ~ 100meters
627       secgeo := ST_Buffer(geometry, 0.001);
628       x := insertLocationAreaRoadFar(partition, place_id, country_code, keywords, rank_search, rank_address, false, ST_Centroid(geometry), secgeo);
629
630     END IF;
631
632     RETURN true;
633
634   END IF;
635
636   RETURN false;
637 END;
638 $$
639 LANGUAGE plpgsql;
640
641 CREATE OR REPLACE FUNCTION update_location(
642     place_id INTEGER,
643     place_country_code varchar(2),
644     name hstore,
645     rank_search INTEGER,
646     rank_address INTEGER,
647     geometry GEOMETRY
648   ) 
649   RETURNS BOOLEAN
650   AS $$
651 DECLARE
652   b BOOLEAN;
653 BEGIN
654   b := delete_location(place_id);
655   RETURN add_location(place_id, place_country_code, name, rank_search, rank_address, geometry);
656 END;
657 $$
658 LANGUAGE plpgsql;
659
660 CREATE OR REPLACE FUNCTION search_name_add_words(parent_place_id INTEGER, to_add INTEGER[])
661   RETURNS BOOLEAN
662   AS $$
663 DECLARE
664   childplace RECORD;
665 BEGIN
666
667   IF #to_add = 0 THEN
668     RETURN true;
669   END IF;
670
671   -- this should just be an update, but it seems to do insane things to the index size (delete and insert doesn't)
672   FOR childplace IN select * from search_name,place_addressline 
673     where  address_place_id = parent_place_id
674       and search_name.place_id = place_addressline.place_id
675   LOOP
676     delete from search_name where place_id = childplace.place_id;
677     childplace.nameaddress_vector := uniq(sort_asc(childplace.nameaddress_vector + to_add));
678     IF childplace.place_id = parent_place_id THEN
679       childplace.name_vector := uniq(sort_asc(childplace.name_vector + to_add));
680     END IF;
681     insert into search_name (place_id, search_rank, address_rank, country_code, name_vector, nameaddress_vector, centroid) 
682       values (childplace.place_id, childplace.search_rank, childplace.address_rank, childplace.country_code, 
683         childplace.name_vector, childplace.nameaddress_vector, childplace.centroid);
684   END LOOP;
685
686   RETURN true;
687 END;
688 $$
689 LANGUAGE plpgsql;
690
691 CREATE OR REPLACE FUNCTION update_location_nameonly(OLD_place_id INTEGER, name hstore) RETURNS BOOLEAN
692   AS $$
693 DECLARE
694   newkeywords INTEGER[];
695   addedkeywords INTEGER[];
696   removedkeywords INTEGER[];
697 BEGIN
698
699   -- what has changed?
700   newkeywords := make_keywords(name);
701   select coalesce(newkeywords,'{}'::INTEGER[]) - coalesce(location_point.keywords,'{}'::INTEGER[]), 
702     coalesce(location_point.keywords,'{}'::INTEGER[]) - coalesce(newkeywords,'{}'::INTEGER[]) from location_point 
703     where place_id = OLD_place_id into addedkeywords, removedkeywords;
704
705 --  RAISE WARNING 'update_location_nameonly for %: new:% added:% removed:%', OLD_place_id, newkeywords, addedkeywords, removedkeywords;
706
707   IF #removedkeywords > 0 THEN
708     -- abort due to tokens removed
709     RETURN false;
710   END IF;
711   
712   IF #addedkeywords > 0 THEN
713     -- short circuit - no changes
714     RETURN true;
715   END IF;
716
717   UPDATE location_area set keywords = newkeywords where place_id = OLD_place_id;
718   RETURN search_name_add_words(OLD_place_id, addedkeywords);
719 END;
720 $$
721 LANGUAGE plpgsql;
722
723
724 CREATE OR REPLACE FUNCTION create_interpolation(wayid INTEGER, interpolationtype TEXT) RETURNS INTEGER
725   AS $$
726 DECLARE
727   
728   newpoints INTEGER;
729   waynodes integer[];
730   nodeid INTEGER;
731   prevnode RECORD;
732   nextnode RECORD;
733   startnumber INTEGER;
734   endnumber INTEGER;
735   stepsize INTEGER;
736   orginalstartnumber INTEGER;
737   originalnumberrange INTEGER;
738   housenum INTEGER;
739   linegeo GEOMETRY;
740   search_place_id INTEGER;
741
742   havefirstpoint BOOLEAN;
743   linestr TEXT;
744 BEGIN
745   newpoints := 0;
746   IF interpolationtype = 'odd' OR interpolationtype = 'even' OR interpolationtype = 'all' THEN
747
748     select nodes from planet_osm_ways where id = wayid INTO waynodes;
749 --RAISE WARNING 'interpolation % % %',wayid,interpolationtype,waynodes;
750     IF array_upper(waynodes, 1) IS NOT NULL THEN
751
752       havefirstpoint := false;
753
754       FOR nodeidpos in 1..array_upper(waynodes, 1) LOOP
755
756         select min(place_id) from placex where osm_type = 'N' and osm_id = waynodes[nodeidpos]::INTEGER and type = 'house' INTO search_place_id;
757         IF search_place_id IS NULL THEN
758           -- null record of right type
759           select * from placex where osm_type = 'N' and osm_id = waynodes[nodeidpos]::INTEGER and type = 'house' limit 1 INTO nextnode;
760           select ST_SetSRID(ST_Point(lon::float/10000000,lat::float/10000000),4326) from planet_osm_nodes where id = waynodes[nodeidpos] INTO nextnode.geometry;
761         ELSE
762           select * from placex where place_id = search_place_id INTO nextnode;
763         END IF;
764
765 --RAISE WARNING 'interpolation node % % % ',nextnode.housenumber,ST_X(nextnode.geometry),ST_Y(nextnode.geometry);
766       
767         IF havefirstpoint THEN
768
769           -- add point to the line string
770           linestr := linestr||','||ST_X(nextnode.geometry)||' '||ST_Y(nextnode.geometry);
771           endnumber := ('0'||substring(nextnode.housenumber,'[0-9]+'))::integer;
772
773           IF startnumber IS NOT NULL and startnumber > 0 AND endnumber IS NOT NULL and endnumber > 0 THEN
774
775 --RAISE WARNING 'interpolation end % % ',nextnode.place_id,endnumber;
776
777             IF startnumber != endnumber THEN
778
779               linestr := linestr || ')';
780 --RAISE WARNING 'linestr %',linestr;
781               linegeo := ST_GeomFromText(linestr,4326);
782               linestr := 'LINESTRING('||ST_X(nextnode.geometry)||' '||ST_Y(nextnode.geometry);
783               IF (startnumber > endnumber) THEN
784                 housenum := endnumber;
785                 endnumber := startnumber;
786                 startnumber := housenum;
787                 linegeo := ST_Reverse(linegeo);
788               END IF;
789               orginalstartnumber := startnumber;
790               originalnumberrange := endnumber - startnumber;
791
792 -- Too much broken data worldwide for this test to be worth using
793 --              IF originalnumberrange > 500 THEN
794 --                RAISE WARNING 'Number block of % while processing % %', originalnumberrange, prevnode, nextnode;
795 --              END IF;
796
797               IF (interpolationtype = 'odd' AND startnumber%2 = 0) OR (interpolationtype = 'even' AND startnumber%2 = 1) THEN
798                 startnumber := startnumber + 1;
799                 stepsize := 2;
800               ELSE
801                 IF (interpolationtype = 'odd' OR interpolationtype = 'even') THEN
802                   startnumber := startnumber + 2;
803                   stepsize := 2;
804                 ELSE -- everything else assumed to be 'all'
805                   startnumber := startnumber + 1;
806                   stepsize := 1;
807                 END IF;
808               END IF;
809               endnumber := endnumber - 1;
810               delete from placex where osm_type = 'N' and osm_id = prevnode.osm_id and type = 'house' and place_id != prevnode.place_id;
811               FOR housenum IN startnumber..endnumber BY stepsize LOOP
812                 -- this should really copy postcodes but it puts a huge burdon on the system for no big benefit
813                 -- ideally postcodes should move up to the way
814                 insert into placex (osm_type, osm_id, class, type, admin_level, housenumber, street, isin, 
815                   country_code, street_place_id, rank_address, rank_search, indexed_status, geometry)
816                   values ('N',prevnode.osm_id, prevnode.class, prevnode.type, prevnode.admin_level, housenum, prevnode.street, prevnode.isin, 
817                   prevnode.country_code, prevnode.street_place_id, prevnode.rank_address, prevnode.rank_search, 1, ST_Line_Interpolate_Point(linegeo, (housenum::float-orginalstartnumber::float)/originalnumberrange::float));
818                 newpoints := newpoints + 1;
819 --RAISE WARNING 'interpolation number % % ',prevnode.place_id,housenum;
820               END LOOP;
821             END IF;
822             havefirstpoint := false;
823           END IF;
824         END IF;
825
826         IF NOT havefirstpoint THEN
827           startnumber := ('0'||substring(nextnode.housenumber,'[0-9]+'))::integer;
828           IF startnumber IS NOT NULL AND startnumber > 0 THEN
829             havefirstpoint := true;
830             linestr := 'LINESTRING('||ST_X(nextnode.geometry)||' '||ST_Y(nextnode.geometry);
831             prevnode := nextnode;
832           END IF;
833 --RAISE WARNING 'interpolation start % % ',nextnode.place_id,startnumber;
834         END IF;
835       END LOOP;
836     END IF;
837   END IF;
838
839 --RAISE WARNING 'interpolation points % ',newpoints;
840
841   RETURN newpoints;
842 END;
843 $$
844 LANGUAGE plpgsql;
845
846 CREATE OR REPLACE FUNCTION placex_insert() RETURNS TRIGGER
847   AS $$
848 DECLARE
849   i INTEGER;
850   postcode TEXT;
851   result BOOLEAN;
852   country_code VARCHAR(2);
853   diameter FLOAT;
854 BEGIN
855 --  RAISE WARNING '%',NEW.osm_id;
856
857   -- just block these
858   IF NEW.class = 'highway' and NEW.type in ('turning_circle','traffic_signals','mini_roundabout','noexit','crossing') THEN
859     RETURN null;
860   END IF;
861   IF NEW.class in ('landuse','natural') and NEW.name is null THEN
862     RETURN null;
863   END IF;
864
865   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  
866     -- block all invalid geometary - just not worth the risk.  seg faults are causing serious problems.
867     RETURN NULL;
868
869     -- Dead code
870     IF NEW.osm_type = 'R' THEN
871       -- invalid multipolygons can crash postgis, don't even bother to try!
872       RETURN NULL;
873     END IF;
874     NEW.geometry := ST_buffer(NEW.geometry,0);
875     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  
876       RAISE WARNING 'Invalid geometary, rejecting: % %', NEW.osm_type, NEW.osm_id;
877       RETURN NULL;
878     END IF;
879   END IF;
880
881   NEW.place_id := nextval('seq_place');
882   NEW.indexed_status := 1;
883   NEW.country_code := lower(NEW.country_code);
884   IF NEW.country_code is null THEN
885     NEW.country_code := get_country_code(NEW.geometry);
886   END IF;
887   NEW.geometry_sector := geometry_sector(NEW.geometry);
888
889   IF NEW.admin_level > 15 THEN
890     NEW.admin_level := 15;
891   END IF;
892
893   IF NEW.housenumber IS NOT NULL THEN
894     i := getorcreate_housenumber_id(make_standard_name(NEW.housenumber));
895   END IF;
896
897   IF NEW.osm_type = 'X' THEN
898     -- E'X'ternal records should already be in the right format so do nothing
899   ELSE
900     NEW.rank_search := 30;
901     NEW.rank_address := NEW.rank_search;
902
903     -- By doing in postgres we have the country available to us - currently only used for postcode
904     IF NEW.class = 'place' THEN
905       IF NEW.type in ('continent') THEN
906         NEW.rank_search := 2;
907         NEW.rank_address := NEW.rank_search;
908       ELSEIF NEW.type in ('sea') THEN
909         NEW.rank_search := 2;
910         NEW.rank_address := 0;
911       ELSEIF NEW.type in ('country') THEN
912         NEW.rank_search := 4;
913         NEW.rank_address := NEW.rank_search;
914       ELSEIF NEW.type in ('state') THEN
915         NEW.rank_search := 8;
916         NEW.rank_address := NEW.rank_search;
917       ELSEIF NEW.type in ('region') THEN
918         NEW.rank_search := 10;
919         NEW.rank_address := NEW.rank_search;
920       ELSEIF NEW.type in ('county') THEN
921         NEW.rank_search := 12;
922         NEW.rank_address := NEW.rank_search;
923       ELSEIF NEW.type in ('city') THEN
924         NEW.rank_search := 16;
925         NEW.rank_address := NEW.rank_search;
926       ELSEIF NEW.type in ('island') THEN
927         NEW.rank_search := 17;
928         NEW.rank_address := 0;
929       ELSEIF NEW.type in ('town') THEN
930         NEW.rank_search := 17;
931         NEW.rank_address := NEW.rank_search;
932       ELSEIF NEW.type in ('village','hamlet','municipality','district','unincorporated_area','borough') THEN
933         NEW.rank_search := 18;
934         NEW.rank_address := 17;
935       ELSEIF NEW.type in ('airport') AND ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon') THEN
936         NEW.rank_search := 18;
937         NEW.rank_address := 17;
938       ELSEIF NEW.type in ('moor') AND ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon') THEN
939         NEW.rank_search := 17;
940         NEW.rank_address := 18;
941       ELSEIF NEW.type in ('moor') THEN
942         NEW.rank_search := 17;
943         NEW.rank_address := 0;
944       ELSEIF NEW.type in ('national_park') THEN
945         NEW.rank_search := 18;
946         NEW.rank_address := 18;
947       ELSEIF NEW.type in ('suburb','croft','subdivision') THEN
948         NEW.rank_search := 20;
949         NEW.rank_address := NEW.rank_search;
950       ELSEIF NEW.type in ('farm','locality','islet') THEN
951         NEW.rank_search := 20;
952         NEW.rank_address := 0;
953       ELSEIF NEW.type in ('hall_of_residence','neighbourhood','housing_estate','nature_reserve') THEN
954         NEW.rank_search := 22;
955         NEW.rank_address := 22;
956       ELSEIF NEW.type in ('postcode') THEN
957
958         -- Postcode processing is very country dependant
959         IF NEW.country_code IS NULL THEN
960         END IF;
961
962         NEW.name := 'ref'=>NEW.postcode;
963
964         IF NEW.country_code = 'gb' THEN
965
966           IF NEW.postcode ~ '^([A-Z][A-Z]?[0-9][0-9A-Z]? [0-9][A-Z][A-Z])$' THEN
967             NEW.rank_search := 25;
968             NEW.rank_address := 5;
969           ELSEIF NEW.postcode ~ '^([A-Z][A-Z]?[0-9][0-9A-Z]? [0-9])$' THEN
970             NEW.rank_search := 23;
971             NEW.rank_address := 5;
972           ELSEIF NEW.postcode ~ '^([A-Z][A-Z]?[0-9][0-9A-Z])$' THEN
973             NEW.rank_search := 21;
974             NEW.rank_address := 5;
975           END IF;
976
977         ELSEIF NEW.country_code = 'de' THEN
978
979           IF NEW.postcode ~ '^([0-9]{5})$' THEN
980             NEW.rank_search := 21;
981             NEW.rank_address := 11;
982           END IF;
983
984         ELSE
985           -- Guess at the postcode format and coverage (!)
986           IF upper(NEW.postcode) ~ '^[A-Z0-9]{1,5}$' THEN -- Probably too short to be very local
987             NEW.rank_search := 21;
988             NEW.rank_address := 11;
989           ELSE
990             -- Does it look splitable into and area and local code?
991             postcode := substring(upper(NEW.postcode) from '^([- :A-Z0-9]+)([- :][A-Z0-9]+)$');
992
993             IF postcode IS NOT NULL THEN
994               NEW.rank_search := 25;
995               NEW.rank_address := 11;
996             ELSEIF NEW.postcode ~ '^[- :A-Z0-9]{6,}$' THEN
997               NEW.rank_search := 21;
998               NEW.rank_address := 11;
999             END IF;
1000           END IF;
1001         END IF;
1002
1003       ELSEIF NEW.type in ('airport','street') THEN
1004         NEW.rank_search := 26;
1005         NEW.rank_address := NEW.rank_search;
1006       ELSEIF NEW.type in ('house','building') THEN
1007         NEW.rank_search := 30;
1008         NEW.rank_address := NEW.rank_search;
1009       ELSEIF NEW.type in ('houses') THEN
1010         -- can't guarantee all required nodes loaded yet due to caching in osm2pgsql
1011         -- insert new point into place for each derived building
1012         --i := create_interpolation(NEW.osm_id, NEW.housenumber);
1013         NEW.rank_search := 28;
1014         NEW.rank_address := 0;
1015       END IF;
1016
1017     ELSEIF NEW.class = 'boundary' THEN
1018       NEW.rank_search := NEW.admin_level * 2;
1019       NEW.rank_address := NEW.rank_search;
1020     ELSEIF NEW.class = 'landuse' AND ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon') THEN
1021       NEW.rank_search := 22;
1022       NEW.rank_address := NEW.rank_search;
1023     -- any feature more than 5 square miles is probably worth indexing
1024     ELSEIF ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon') AND ST_Area(NEW.geometry) > 0.1 THEN
1025       NEW.rank_search := 22;
1026       NEW.rank_address := NEW.rank_search;
1027     ELSEIF NEW.class = 'highway' AND NEW.name is NULL AND 
1028            NEW.type in ('service','cycleway','path','footway','steps','bridleway','track','byway','motorway_link','primary_link','trunk_link','secondary_link','tertiary_link') THEN
1029       RETURN NULL;
1030     ELSEIF NEW.class = 'railway' AND NEW.type in ('rail') THEN
1031       RETURN NULL;
1032     ELSEIF NEW.class = 'waterway' AND NEW.name is NULL THEN
1033       RETURN NULL;
1034     ELSEIF NEW.class = 'waterway' THEN
1035       NEW.rank_address := 17;
1036     ELSEIF NEW.class = 'highway' AND NEW.osm_type != 'N' AND NEW.type in ('service','cycleway','path','footway','steps','bridleway','motorway_link','primary_link','trunk_link','secondary_link','tertiary_link') THEN
1037       NEW.rank_search := 27;
1038       NEW.rank_address := NEW.rank_search;
1039     ELSEIF NEW.class = 'highway' AND NEW.osm_type != 'N' THEN
1040       NEW.rank_search := 26;
1041       NEW.rank_address := NEW.rank_search;
1042     ELSEIF NEW.class = 'natural' and NEW.type = 'sea' THEN
1043       NEW.rank_search := 4;
1044       NEW.rank_address := NEW.rank_search;
1045     ELSEIF NEW.class = 'natural' and NEW.type in ('coastline') THEN
1046       RETURN NULL;
1047     ELSEIF NEW.class = 'natural' and NEW.type in ('peak','volcano') THEN
1048       NEW.rank_search := 18;
1049       NEW.rank_address := 0;
1050     END IF;
1051
1052   END IF;
1053
1054   IF NEW.rank_search > 30 THEN
1055     NEW.rank_search := 30;
1056   END IF;
1057
1058   IF NEW.rank_address > 30 THEN
1059     NEW.rank_address := 30;
1060   END IF;
1061
1062 -- Block import below rank 22
1063 --  IF NEW.rank_search > 22 THEN
1064 --    RETURN NULL;
1065 --  END IF;
1066
1067   IF NEW.name is not null THEN
1068     result := add_location(NEW.place_id,NEW.country_code,NEW.name,NEW.rank_search,NEW.rank_address,NEW.geometry);
1069   END IF;
1070
1071   RETURN NEW;
1072   -- The following is not needed until doing diff updates, and slows the main index process down
1073
1074   IF (ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon') AND ST_IsValid(NEW.geometry)) THEN
1075     -- Performance: We just can't handle re-indexing for country level changes
1076     IF st_area(NEW.geometry) < 1 THEN
1077       -- mark items within the geometry for re-indexing
1078 --    RAISE WARNING 'placex poly insert: % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type;
1079 -- work around bug in postgis
1080       update placex set indexed_status = 2 where (ST_Contains(NEW.geometry, placex.geometry) OR ST_Intersects(NEW.geometry, placex.geometry)) 
1081        AND rank_search > NEW.rank_search and indexed = 0 and ST_geometrytype(placex.geometry) = 'ST_Point';
1082       update placex set indexed_status = 2 where (ST_Contains(NEW.geometry, placex.geometry) OR ST_Intersects(NEW.geometry, placex.geometry)) 
1083        AND rank_search > NEW.rank_search and indexed = 0 and ST_geometrytype(placex.geometry) != 'ST_Point';
1084     END IF;
1085   ELSE
1086     -- mark nearby items for re-indexing, where 'nearby' depends on the features rank_search and is a complete guess :(
1087     diameter := 0;
1088     -- 16 = city, anything higher than city is effectively ignored (polygon required!)
1089     IF NEW.type='postcode' THEN
1090       diameter := 0.001;
1091     ELSEIF NEW.rank_search < 16 THEN
1092       diameter := 0;
1093     ELSEIF NEW.rank_search < 18 THEN
1094       diameter := 0.1;
1095     ELSEIF NEW.rank_search < 20 THEN
1096       diameter := 0.05;
1097     ELSEIF NEW.rank_search = 21 THEN
1098       diameter := 0.001;
1099     ELSEIF NEW.rank_search < 24 THEN
1100       diameter := 0.02;
1101     ELSEIF NEW.rank_search < 26 THEN
1102       diameter := 0.002; -- 100 to 200 meters
1103     ELSEIF NEW.rank_search < 28 THEN
1104       diameter := 0.001; -- 50 to 100 meters
1105     END IF;
1106     IF diameter > 0 THEN
1107 --      RAISE WARNING 'placex point insert: % % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type,diameter;
1108       update placex set indexed = 2 where indexed and rank_search > NEW.rank_search and ST_DWithin(placex.geometry, NEW.geometry, diameter);
1109     END IF;
1110
1111   END IF;
1112
1113 --  IF NEW.rank_search < 26 THEN
1114 --    RAISE WARNING 'placex insert: % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type;
1115 --  END IF;
1116
1117   RETURN NEW;
1118
1119 END;
1120 $$
1121 LANGUAGE plpgsql;
1122
1123 CREATE OR REPLACE FUNCTION placex_update() RETURNS 
1124 TRIGGER
1125   AS $$
1126 DECLARE
1127
1128   place_centroid GEOMETRY;
1129
1130   search_maxdistance FLOAT[];
1131   search_mindistance FLOAT[];
1132   address_havelevel BOOLEAN[];
1133 --  search_scores wordscore[];
1134 --  search_scores_pos INTEGER;
1135
1136   i INTEGER;
1137   iMax FLOAT;
1138   location RECORD;
1139   relation RECORD;
1140   search_diameter FLOAT;
1141   search_prevdiameter FLOAT;
1142   search_maxrank INTEGER;
1143   address_maxrank INTEGER;
1144   address_street_word_id INTEGER;
1145   street_place_id_count INTEGER;
1146   isin TEXT[];
1147   isin_tokens INT[];
1148
1149   location_rank_search INTEGER;
1150   location_distance FLOAT;
1151
1152   tagpairid INTEGER;
1153
1154   name_vector INTEGER[];
1155   nameaddress_vector INTEGER[];
1156
1157   result BOOLEAN;
1158 BEGIN
1159
1160 --RAISE WARNING '%',NEW.place_id;
1161 --RAISE WARNING '%', NEW;
1162
1163   IF NEW.class = 'place' AND NEW.type = 'postcodearea' THEN
1164     -- Silently do nothing
1165     RETURN NEW;
1166   END IF;
1167
1168   IF NEW.country_code is null THEN
1169     NEW.country_code := get_country_code(NEW.geometry);
1170   END IF;
1171   NEW.country_code := lower(NEW.country_code);
1172   NEW.partition := NEW.country_code;
1173   IF NEW.partition is null THEN
1174     NEW.partition := 'none';
1175   END IF;
1176
1177   IF NEW.indexed_status = 0 and OLD.indexed_status != 0 THEN
1178
1179     NEW.indexed_date = now();
1180
1181     IF NEW.class = 'place' AND NEW.type = 'houses' THEN
1182       i := create_interpolation(NEW.osm_id, NEW.housenumber);
1183       RETURN NEW;
1184     END IF;
1185
1186     DELETE FROM search_name WHERE place_id = NEW.place_id;
1187     DELETE FROM place_addressline WHERE place_id = NEW.place_id;
1188     DELETE FROM place_boundingbox where place_id = NEW.place_id;
1189
1190     -- Adding ourselves to the list simplifies address calculations later
1191     INSERT INTO place_addressline VALUES (NEW.place_id, NEW.place_id, true, true, 0, NEW.rank_address); 
1192
1193     -- What level are we searching from
1194     search_maxrank := NEW.rank_search;
1195
1196     -- Speed up searches - just use the centroid of the feature
1197     -- cheaper but less acurate
1198     place_centroid := ST_Centroid(NEW.geometry);
1199
1200     -- Initialise the name vector using our name
1201     name_vector := make_keywords(NEW.name);
1202     nameaddress_vector := '{}'::int[];
1203
1204     -- some tag combinations add a special id for search
1205     tagpairid := get_tagpair(NEW.class,NEW.type);
1206     IF tagpairid IS NOT NULL THEN
1207       name_vector := name_vector + tagpairid;
1208     END IF;
1209
1210 --RAISE WARNING '% %', NEW.place_id, NEW.rank_search;
1211
1212     -- For low level elements we inherit from our parent road
1213     IF (NEW.rank_search > 27 OR (NEW.type = 'postcode' AND NEW.rank_search = 25)) THEN
1214
1215 --RAISE WARNING 'finding street for %', NEW;
1216
1217       NEW.street_place_id := null;
1218
1219       -- to do that we have to find our parent road
1220       -- Copy data from linked items (points on ways, addr:street links, relations)
1221       -- Note that addr:street links can only be indexed once the street itself is indexed
1222       IF NEW.street_place_id IS NULL AND NEW.osm_type = 'N' THEN
1223
1224         -- Is this node part of a relation?
1225         FOR relation IN select * from planet_osm_rels where parts @> ARRAY[NEW.osm_id::integer] and members @> ARRAY['n'||NEW.osm_id]
1226         LOOP
1227           -- At the moment we only process one type of relation - associatedStreet
1228           IF relation.tags @> ARRAY['associatedStreet'] AND array_upper(relation.members, 1) IS NOT NULL THEN
1229             FOR i IN 1..array_upper(relation.members, 1) BY 2 LOOP
1230               IF NEW.street_place_id IS NULL AND relation.members[i+1] = 'street' THEN
1231 --RAISE WARNING 'node in relation %',relation;
1232                 SELECT place_id from placex where osm_type='W' and osm_id = substring(relation.members[i],2,200)::integer 
1233                   and rank_search = 26 INTO NEW.street_place_id;
1234               END IF;
1235             END LOOP;
1236           END IF;
1237         END LOOP;      
1238
1239 --RAISE WARNING 'x1';
1240         -- Is this node part of a way?
1241         FOR location IN select * from placex where osm_type = 'W' 
1242           and osm_id in (select id from planet_osm_ways where nodes && ARRAY[NEW.osm_id::integer])
1243         LOOP
1244 --RAISE WARNING '%', location;
1245           -- Way IS a road then we are on it - that must be our road
1246           IF location.rank_search = 26 AND NEW.street_place_id IS NULL THEN
1247 --RAISE WARNING 'node in way that is a street %',location;
1248             NEW.street_place_id := location.place_id;
1249           END IF;
1250
1251           -- Is the WAY part of a relation
1252           FOR relation IN select * from planet_osm_rels where parts @> ARRAY[location.osm_id::integer] and members @> ARRAY['w'||location.osm_id]
1253           LOOP
1254             -- At the moment we only process one type of relation - associatedStreet
1255             IF relation.tags @> ARRAY['associatedStreet'] AND array_upper(relation.members, 1) IS NOT NULL THEN
1256               FOR i IN 1..array_upper(relation.members, 1) BY 2 LOOP
1257                 IF NEW.street_place_id IS NULL AND relation.members[i+1] = 'street' THEN
1258 --RAISE WARNING 'node in way that is in a relation %',relation;
1259                   SELECT place_id from placex where osm_type='W' and osm_id = substring(relation.members[i],2,200)::integer 
1260                     and rank_search = 26 INTO NEW.street_place_id;
1261                 END IF;
1262               END LOOP;
1263             END IF;
1264           END LOOP;
1265           
1266           -- If the way contains an explicit name of a street copy it
1267           IF NEW.street IS NULL AND location.street IS NOT NULL THEN
1268 --RAISE WARNING 'node in way that has a streetname %',location;
1269             NEW.street := location.street;
1270           END IF;
1271
1272           -- If this way is a street interpolation line then it is probably as good as we are going to get
1273           IF NEW.street_place_id IS NULL AND NEW.street IS NULL AND location.class = 'place' and location.type='houses' THEN
1274             -- Try and find a way that is close roughly parellel to this line
1275             FOR relation IN SELECT place_id FROM placex
1276               WHERE ST_DWithin(location.geometry, placex.geometry, 0.001) and placex.rank_search = 26
1277                 and st_geometrytype(location.geometry) in ('ST_LineString')
1278               ORDER BY (ST_distance(placex.geometry, ST_Line_Interpolate_Point(location.geometry,0))+
1279                         ST_distance(placex.geometry, ST_Line_Interpolate_Point(location.geometry,0.5))+
1280                         ST_distance(placex.geometry, ST_Line_Interpolate_Point(location.geometry,1))) ASC limit 1
1281             LOOP
1282 --RAISE WARNING 'using nearest street to address interpolation line,0.001 %',relation;
1283               NEW.street_place_id := relation.place_id;
1284             END LOOP;
1285           END IF;
1286
1287         END LOOP;
1288                 
1289       END IF;
1290
1291 --RAISE WARNING 'x2';
1292
1293       IF NEW.street_place_id IS NULL AND NEW.osm_type = 'W' THEN
1294         -- Is this way part of a relation?
1295         FOR relation IN select * from planet_osm_rels where parts @> ARRAY[NEW.osm_id::integer] and members @> ARRAY['w'||NEW.osm_id]
1296         LOOP
1297           -- At the moment we only process one type of relation - associatedStreet
1298           IF relation.tags @> ARRAY['associatedStreet'] AND array_upper(relation.members, 1) IS NOT NULL THEN
1299             FOR i IN 1..array_upper(relation.members, 1) BY 2 LOOP
1300               IF NEW.street_place_id IS NULL AND relation.members[i+1] = 'street' THEN
1301 --RAISE WARNING 'way that is in a relation %',relation;
1302                 SELECT place_id from placex where osm_type='W' and osm_id = substring(relation.members[i],2,200)::integer
1303                   and rank_search = 26 INTO NEW.street_place_id;
1304               END IF;
1305             END LOOP;
1306           END IF;
1307         END LOOP;
1308       END IF;
1309       
1310 --RAISE WARNING 'x3';
1311
1312       IF NEW.street_place_id IS NULL AND NEW.street IS NOT NULL THEN
1313         address_street_word_id := get_name_id(make_standard_name(NEW.street));
1314 --RAISE WARNING 'street: % %', NEW.street, address_street_word_id;
1315         IF address_street_word_id IS NOT NULL THEN
1316           FOR location IN SELECT place_id,ST_distance(NEW.geometry, search_name.centroid) as distance 
1317             FROM search_name WHERE search_name.name_vector @> ARRAY[address_street_word_id]
1318             AND ST_DWithin(NEW.geometry, search_name.centroid, 0.01) and search_rank between 22 and 27
1319             ORDER BY ST_distance(NEW.geometry, search_name.centroid) ASC limit 1
1320           LOOP
1321 --RAISE WARNING 'streetname found nearby %',location;
1322             NEW.street_place_id := location.place_id;
1323           END LOOP;
1324         END IF;
1325         -- Failed, fall back to nearest - don't just stop
1326         IF NEW.street_place_id IS NULL THEN
1327 --RAISE WARNING 'unable to find streetname nearby % %',NEW.street,address_street_word_id;
1328 --          RETURN null;
1329         END IF;
1330       END IF;
1331
1332 --RAISE WARNING 'x4';
1333
1334       IF NEW.street_place_id IS NULL THEN
1335         FOR location IN SELECT place_id FROM getNearRoads(NEW.partition, place_centroid) LOOP
1336           NEW.street_place_id := location.place_id;
1337         END LOOP;
1338       END IF;
1339
1340 --RAISE WARNING 'x6 %',NEW.street_place_id;
1341
1342       -- If we didn't find any road fallback to standard method
1343       IF NEW.street_place_id IS NOT NULL THEN
1344
1345         -- Some unnamed roads won't have been indexed, index now if needed
1346 -- ALL are now indexed!
1347 --        select count(*) from place_addressline where place_id = NEW.street_place_id INTO street_place_id_count;
1348 --        IF street_place_id_count = 0 THEN
1349 --          UPDATE placex set indexed = true where indexed = false and place_id = NEW.street_place_id;
1350 --        END IF;
1351
1352         -- Add the street to the address as zero distance to force to front of list
1353         INSERT INTO place_addressline VALUES (NEW.place_id, NEW.street_place_id, true, true, 0, 26);
1354         address_havelevel[26] := true;
1355
1356         -- Import address details from parent, reclculating distance in process
1357         INSERT INTO place_addressline select NEW.place_id, x.address_place_id, x.fromarea, x.isaddress, ST_distance(NEW.geometry, placex.geometry), placex.rank_address
1358           from place_addressline as x join placex on (address_place_id = placex.place_id)
1359           where x.place_id = NEW.street_place_id and x.address_place_id != NEW.street_place_id;
1360
1361         -- Get the details of the parent road
1362         select * from search_name where place_id = NEW.street_place_id INTO location;
1363         NEW.country_code := location.country_code;
1364
1365 --RAISE WARNING '%', NEW.name;
1366         -- If there is no name it isn't searchable, don't bother to create a search record
1367         IF NEW.name is NULL THEN
1368           return NEW;
1369         END IF;
1370
1371         -- Merge address from parent
1372         nameaddress_vector := array_merge(nameaddress_vector, location.nameaddress_vector);
1373
1374         -- Performance, it would be more acurate to do all the rest of the import process but it takes too long
1375         -- Just be happy with inheriting from parent road only
1376         INSERT INTO search_name values (NEW.place_id, NEW.rank_search, NEW.rank_address, NEW.country_code,
1377           name_vector, nameaddress_vector, place_centroid);
1378
1379         return NEW;
1380       END IF;
1381
1382     END IF;
1383
1384 --RAISE WARNING '  INDEXING: %',NEW;
1385
1386     -- convert isin to array of tokenids
1387     isin_tokens := '{}'::int[];
1388     IF NEW.isin IS NOT NULL THEN
1389       isin := regexp_split_to_array(NEW.isin, E'[;,]');
1390       IF array_upper(isin, 1) IS NOT NULL THEN
1391         FOR i IN 1..array_upper(isin, 1) LOOP
1392           address_street_word_id := get_name_id(make_standard_name(isin[i]));
1393           IF address_street_word_id IS NOT NULL THEN
1394             isin_tokens := isin_tokens + address_street_word_id;
1395           END IF;
1396         END LOOP;
1397       END IF;
1398       isin_tokens := uniq(sort(isin_tokens));
1399     END IF;
1400
1401     -- Process area matches
1402     location_rank_search := 100;
1403     location_distance := 0;
1404     FOR location IN SELECT * from getNearFeatures(NEW.partition, place_centroid, search_maxrank, isin_tokens) LOOP
1405
1406 --RAISE WARNING '  AREA: % % %',location.keywords,NEW.country_code,location.country_code;
1407
1408       IF location.rank_search < location_rank_search THEN
1409         location_rank_search := location.rank_search;
1410         location_distance := location.distance * 1.5;
1411       END IF;
1412
1413       IF location.distance < location_distance THEN
1414
1415         -- Add it to the list of search terms
1416         nameaddress_vector := array_merge(nameaddress_vector, location.keywords::integer[]);
1417         INSERT INTO place_addressline VALUES (NEW.place_id, location.place_id, true, NOT address_havelevel[location.rank_address], location.distance, location.rank_address); 
1418         address_havelevel[location.rank_address] := true;
1419
1420       END IF;
1421
1422     END LOOP;
1423
1424     -- try using the isin value to find parent places
1425     IF array_upper(isin_tokens, 1) IS NOT NULL THEN
1426       FOR i IN 1..array_upper(isin_tokens, 1) LOOP
1427
1428         FOR location IN SELECT place_id,search_name.name_vector,address_rank,
1429           ST_Distance(place_centroid, search_name.centroid) as distance
1430           FROM search_name
1431           WHERE search_name.name_vector @> ARRAY[isin_tokens[i]]
1432           AND search_rank < NEW.rank_search
1433           AND (country_code = NEW.country_code OR address_rank < 4)
1434           ORDER BY ST_distance(NEW.geometry, centroid) ASC limit 1
1435         LOOP
1436           nameaddress_vector := array_merge(nameaddress_vector, location.name_vector);
1437           INSERT INTO place_addressline VALUES (NEW.place_id, location.place_id, false, NOT address_havelevel[location.address_rank], location.distance, location.address_rank);
1438         END LOOP;
1439
1440       END LOOP;
1441     END IF;
1442
1443     -- if we have a name add this to the name search table
1444     IF NEW.name IS NOT NULL THEN
1445       INSERT INTO search_name values (NEW.place_id, NEW.rank_search, NEW.rank_search, NEW.country_code, 
1446         name_vector, nameaddress_vector, place_centroid);
1447     END IF;
1448
1449   END IF;
1450
1451   RETURN NEW;
1452 END;
1453 $$
1454 LANGUAGE plpgsql;
1455
1456 CREATE OR REPLACE FUNCTION placex_delete() RETURNS TRIGGER
1457   AS $$
1458 DECLARE
1459 BEGIN
1460
1461 --IF OLD.rank_search < 26 THEN
1462 --RAISE WARNING 'delete % % % % %',OLD.place_id,OLD.osm_type,OLD.osm_id,OLD.class,OLD.type;
1463 --END IF;
1464
1465   -- mark everything linked to this place for re-indexing
1466   UPDATE placex set indexed_status = 2 from place_addressline where address_place_id = OLD.place_id 
1467     and placex.place_id = place_addressline.place_id and indexed_status = 0;
1468
1469   -- do the actual delete
1470   DELETE FROM location_area where place_id = OLD.place_id;
1471   DELETE FROM search_name where place_id = OLD.place_id;
1472   DELETE FROM place_addressline where place_id = OLD.place_id;
1473   DELETE FROM place_addressline where address_place_id = OLD.place_id;
1474
1475   RETURN OLD;
1476
1477 END;
1478 $$
1479 LANGUAGE plpgsql;
1480
1481 CREATE OR REPLACE FUNCTION place_delete() RETURNS TRIGGER
1482   AS $$
1483 DECLARE
1484   placeid INTEGER;
1485 BEGIN
1486
1487 --  RAISE WARNING 'delete: % % % %',OLD.osm_type,OLD.osm_id,OLD.class,OLD.type;
1488   delete from placex where osm_type = OLD.osm_type and osm_id = OLD.osm_id and class = OLD.class and type = OLD.type;
1489   RETURN OLD;
1490
1491 END;
1492 $$
1493 LANGUAGE plpgsql;
1494
1495 CREATE OR REPLACE FUNCTION place_insert() RETURNS TRIGGER
1496   AS $$
1497 DECLARE
1498   i INTEGER;
1499   existing RECORD;
1500   existingplacex RECORD;
1501   existinggeometry GEOMETRY;
1502   existingplace_id INTEGER;
1503   result BOOLEAN;
1504 BEGIN
1505
1506   IF FALSE AND NEW.osm_type = 'R' THEN
1507     RAISE WARNING '-----------------------------------------------------------------------------------';
1508     RAISE WARNING 'place_insert: % % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type,st_area(NEW.geometry);
1509     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;
1510     RAISE WARNING '%', existingplacex;
1511   END IF;
1512
1513   -- Just block these - lots and pointless
1514   IF NEW.class = 'highway' and NEW.type in ('turning_circle','traffic_signals','mini_roundabout','noexit','crossing') THEN
1515     RETURN null;
1516   END IF;
1517   IF NEW.class in ('landuse','natural') and NEW.name is null THEN
1518     RETURN null;
1519   END IF;
1520
1521   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  
1522 --    RAISE WARNING 'Invalid Geometry: % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type;
1523     RETURN null;
1524   END IF;
1525
1526   -- Patch in additional country names
1527   -- adminitrative (with typo) is unfortunately hard codes - this probably won't get fixed until v2
1528   IF NEW.admin_level = 2 AND NEW.type = 'adminitrative' AND NEW.country_code is not null THEN
1529     select country_name.name || NEW.name from country_name where country_name.country_code = lower(NEW.country_code) INTO NEW.name;
1530   END IF;
1531     
1532   -- Have we already done this place?
1533   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;
1534
1535   -- Get the existing place_id
1536   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;
1537
1538   -- Handle a place changing type by removing the old data
1539   -- My generated 'place' types are causing havok because they overlap with real tags
1540   -- TODO: move them to their own special purpose tag to avoid collisions
1541   IF existing.osm_type IS NULL AND (NEW.type not in ('postcode','house','houses')) THEN
1542     DELETE FROM place where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class and type not in ('postcode','house','houses');
1543   END IF;
1544
1545 --  RAISE WARNING 'Existing: %',existing.place_id;
1546
1547   -- To paraphrase, if there isn't an existing item, OR if the admin level has changed, OR if it is a major change in geometry
1548   IF existing.osm_type IS NULL 
1549      OR existingplacex.osm_type IS NULL
1550      OR coalesce(existing.admin_level, 100) != coalesce(NEW.admin_level, 100) 
1551 --     OR coalesce(existing.country_code, '') != coalesce(NEW.country_code, '')
1552      OR (existing.geometry != NEW.geometry AND ST_Distance(ST_Centroid(existing.geometry),ST_Centroid(NEW.geometry)) > 0.01 AND NOT
1553      (ST_GeometryType(existing.geometry) in ('ST_Polygon','ST_MultiPolygon') AND ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon')))
1554      THEN
1555
1556 --  IF existing.osm_type IS NULL THEN
1557 --    RAISE WARNING 'no existing place';
1558 --  END IF;
1559 --  IF existingplacex.osm_type IS NULL THEN
1560 --    RAISE WARNING 'no existing placex %', existingplacex;
1561 --  END IF;
1562
1563
1564 --    RAISE WARNING 'delete and replace';
1565
1566     IF existing.osm_type IS NOT NULL THEN
1567 --      RAISE WARNING 'insert delete % % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type,ST_Distance(ST_Centroid(existing.geometry),ST_Centroid(NEW.geometry)),existing;
1568       IF existing.rank_search < 26 THEN
1569 --        RAISE WARNING 'replace placex % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type;
1570       END IF;
1571       DELETE FROM place where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class and type = NEW.type;
1572     END IF;   
1573
1574 --    RAISE WARNING 'delete and replace2';
1575
1576     -- No - process it as a new insertion (hopefully of low rank or it will be slow)
1577     insert into placex values (NEW.place_id
1578         ,NEW.osm_type
1579         ,NEW.osm_id
1580         ,NEW.class
1581         ,NEW.type
1582         ,NEW.name
1583         ,NEW.admin_level
1584         ,NEW.housenumber
1585         ,NEW.street
1586         ,NEW.isin
1587         ,NEW.postcode
1588         ,NEW.country_code
1589         ,NEW.street_place_id
1590         ,NEW.rank_address
1591         ,NEW.rank_search
1592         ,NEW.indexed
1593         ,NEW.geometry
1594         );
1595
1596 --    RAISE WARNING 'insert done % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type;
1597
1598     RETURN NEW;
1599   END IF;
1600
1601   -- Various ways to do the update
1602
1603   -- Debug, what's changed?
1604   IF FALSE AND existing.rank_search < 26 THEN
1605     IF coalesce(existing.name::text, '') != coalesce(NEW.name::text, '') THEN
1606       RAISE WARNING 'update details, name: % % % %',NEW.osm_type,NEW.osm_id,existing.name::text,NEW.name::text;
1607     END IF;
1608     IF coalesce(existing.housenumber, '') != coalesce(NEW.housenumber, '') THEN
1609       RAISE WARNING 'update details, housenumber: % % % %',NEW.osm_type,NEW.osm_id,existing.housenumber,NEW.housenumber;
1610     END IF;
1611     IF coalesce(existing.street, '') != coalesce(NEW.street, '') THEN
1612       RAISE WARNING 'update details, street: % % % %',NEW.osm_type,NEW.osm_id,existing.street,NEW.street;
1613     END IF;
1614     IF coalesce(existing.isin, '') != coalesce(NEW.isin, '') THEN
1615       RAISE WARNING 'update details, isin: % % % %',NEW.osm_type,NEW.osm_id,existing.isin,NEW.isin;
1616     END IF;
1617     IF coalesce(existing.postcode, '') != coalesce(NEW.postcode, '') THEN
1618       RAISE WARNING 'update details, postcode: % % % %',NEW.osm_type,NEW.osm_id,existing.postcode,NEW.postcode;
1619     END IF;
1620     IF coalesce(existing.country_code, '') != coalesce(NEW.country_code, '') THEN
1621       RAISE WARNING 'update details, country_code: % % % %',NEW.osm_type,NEW.osm_id,existing.country_code,NEW.country_code;
1622     END IF;
1623   END IF;
1624
1625   -- Special case for polygon shape changes because they tend to be large and we can be a bit clever about how we handle them
1626   IF existing.geometry != NEW.geometry 
1627      AND ST_GeometryType(existing.geometry) in ('ST_Polygon','ST_MultiPolygon')
1628      AND ST_GeometryType(NEW.geometry) in ('ST_Polygon','ST_MultiPolygon') 
1629      THEN 
1630
1631 --    IF existing.rank_search < 26 THEN
1632 --      RAISE WARNING 'existing polygon change % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type;
1633 --    END IF;
1634
1635     -- Get the version of the geometry actually used (in placex table)
1636     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;
1637
1638     -- Performance limit
1639     IF st_area(NEW.geometry) < 1 AND st_area(existinggeometry) < 1 THEN
1640
1641       -- 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
1642       update placex set indexed_status = 2 where indexed_status = 0 and 
1643           (ST_Contains(NEW.geometry, placex.geometry) OR ST_Intersects(NEW.geometry, placex.geometry))
1644           AND NOT (ST_Contains(existinggeometry, placex.geometry) OR ST_Intersects(existinggeometry, placex.geometry))
1645           AND rank_search > NEW.rank_search;
1646
1647       update placex set indexed_status = 2 where indexed_status = 0 and 
1648           (ST_Contains(existinggeometry, placex.geometry) OR ST_Intersects(existinggeometry, placex.geometry))
1649           AND NOT (ST_Contains(NEW.geometry, placex.geometry) OR ST_Intersects(NEW.geometry, placex.geometry))
1650           AND rank_search > NEW.rank_search;
1651
1652     END IF;
1653
1654   END IF;
1655
1656   -- Special case - if we are just adding extra words we hack them into the search_name table rather than reindexing
1657   IF existingplacex.rank_search < 26
1658      AND coalesce(existing.housenumber, '') = coalesce(NEW.housenumber, '')
1659      AND coalesce(existing.street, '') = coalesce(NEW.street, '')
1660      AND coalesce(existing.isin, '') = coalesce(NEW.isin, '')
1661      AND coalesce(existing.postcode, '') = coalesce(NEW.postcode, '')
1662      AND coalesce(existing.country_code, '') = coalesce(NEW.country_code, '')
1663      AND coalesce(existing.name::text, '') != coalesce(NEW.name::text, '') 
1664      THEN
1665
1666 --    IF existing.rank_search < 26 THEN
1667 --      RAISE WARNING 'name change only % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type;
1668 --    END IF;
1669
1670     IF NOT update_location_nameonly(existingplacex.place_id, NEW.name) THEN
1671
1672       IF st_area(NEW.geometry) < 0.5 THEN
1673         UPDATE placex set indexed_status = 2 from place_addressline where address_place_id = existingplacex.place_id 
1674           and placex.place_id = place_addressline.place_id and indexed_status = 0;
1675       END IF;
1676
1677     END IF;
1678   
1679   ELSE
1680
1681     -- Anything else has changed - reindex the lot
1682     IF coalesce(existing.name::text, '') != coalesce(NEW.name::text, '')
1683         OR coalesce(existing.housenumber, '') != coalesce(NEW.housenumber, '')
1684         OR coalesce(existing.street, '') != coalesce(NEW.street, '')
1685         OR coalesce(existing.isin, '') != coalesce(NEW.isin, '')
1686         OR coalesce(existing.postcode, '') != coalesce(NEW.postcode, '')
1687         OR coalesce(existing.country_code, '') != coalesce(NEW.country_code, '') THEN
1688
1689 --      IF existing.rank_search < 26 THEN
1690 --        RAISE WARNING 'other change % % % %',NEW.osm_type,NEW.osm_id,NEW.class,NEW.type;
1691 --      END IF;
1692
1693       -- performance, can't take the load of re-indexing a whole country / huge area
1694       IF st_area(NEW.geometry) < 0.5 THEN
1695         UPDATE placex set indexed_status = 2 from place_addressline where address_place_id = existingplacex.place_id 
1696           and placex.place_id = place_addressline.place_id and indexed_status = 0;
1697       END IF;
1698
1699     END IF;
1700
1701   END IF;
1702
1703   IF coalesce(existing.name::text, '') != coalesce(NEW.name::text, '')
1704      OR coalesce(existing.housenumber, '') != coalesce(NEW.housenumber, '')
1705      OR coalesce(existing.street, '') != coalesce(NEW.street, '')
1706      OR coalesce(existing.isin, '') != coalesce(NEW.isin, '')
1707      OR coalesce(existing.postcode, '') != coalesce(NEW.postcode, '')
1708      OR coalesce(existing.country_code, '') != coalesce(NEW.country_code, '')
1709      OR existing.geometry != NEW.geometry
1710      THEN
1711
1712     update place set 
1713       name = NEW.name,
1714       housenumber  = NEW.housenumber,
1715       street = NEW.street,
1716       isin = NEW.isin,
1717       postcode = NEW.postcode,
1718       country_code = NEW.country_code,
1719       street_place_id = null,
1720       geometry = NEW.geometry
1721       where osm_type = NEW.osm_type and osm_id = NEW.osm_id and class = NEW.class and type = NEW.type;
1722
1723     update placex set 
1724       name = NEW.name,
1725       housenumber = NEW.housenumber,
1726       street = NEW.street,
1727       isin = NEW.isin,
1728       postcode = NEW.postcode,
1729       country_code = NEW.country_code,
1730       street_place_id = null,
1731       indexed = false,
1732       geometry = NEW.geometry
1733       where place_id = existingplacex.place_id;
1734
1735     result := update_location(existingplacex.place_id, existingplacex.country_code, NEW.name, existingplacex.rank_search, existingplacex.rank_address, NEW.geometry);
1736
1737   END IF;
1738
1739   -- Abort the add (we modified the existing place instead)
1740   RETURN NULL;
1741
1742 END; 
1743 $$ LANGUAGE plpgsql;
1744
1745 CREATE OR REPLACE FUNCTION get_name_by_language(name hstore, languagepref TEXT[]) RETURNS TEXT
1746   AS $$
1747 DECLARE
1748   search TEXT[];
1749   found BOOLEAN;
1750 BEGIN
1751
1752   IF name is null THEN
1753     RETURN null;
1754   END IF;
1755
1756   search := languagepref;
1757
1758   FOR j IN 1..array_upper(search, 1) LOOP
1759     IF name ? search[j] AND trim(name->search[j]) != '' THEN
1760       return trim(name->search[j]);
1761     END IF;
1762   END LOOP;
1763
1764   RETURN null;
1765 END;
1766 $$
1767 LANGUAGE plpgsql IMMUTABLE;
1768
1769 CREATE OR REPLACE FUNCTION get_connected_ways(way_ids INTEGER[]) RETURNS SETOF planet_osm_ways
1770   AS $$
1771 DECLARE
1772   searchnodes INTEGER[];
1773   location RECORD;
1774   j INTEGER;
1775 BEGIN
1776
1777   searchnodes := '{}';
1778   FOR j IN 1..array_upper(way_ids, 1) LOOP
1779     FOR location IN 
1780       select nodes from planet_osm_ways where id = way_ids[j] LIMIT 1
1781     LOOP
1782       searchnodes := searchnodes | location.nodes;
1783     END LOOP;
1784   END LOOP;
1785
1786   RETURN QUERY select * from planet_osm_ways where nodes && searchnodes and NOT ARRAY[id] <@ way_ids;
1787 END;
1788 $$
1789 LANGUAGE plpgsql IMMUTABLE;
1790
1791 CREATE OR REPLACE FUNCTION get_address_postcode(for_place_id INTEGER) RETURNS TEXT
1792   AS $$
1793 DECLARE
1794   result TEXT[];
1795   search TEXT[];
1796   for_postcode TEXT;
1797   found INTEGER;
1798   location RECORD;
1799 BEGIN
1800
1801   found := 1000;
1802   search := ARRAY['ref'];
1803   result := '{}';
1804
1805   UPDATE placex set indexed = true where indexed = false and place_id = for_place_id;
1806
1807   select postcode from placex where place_id = for_place_id limit 1 into for_postcode;
1808
1809   FOR location IN 
1810     select rank_address,name,distance,length(name::text) as namelength 
1811       from place_addressline join placex on (address_place_id = placex.place_id) 
1812       where place_addressline.place_id = for_place_id and rank_address in (5,11)
1813       order by rank_address desc,rank_search desc,fromarea desc,distance asc,namelength desc
1814   LOOP
1815     IF array_upper(search, 1) IS NOT NULL AND array_upper(location.name, 1) IS NOT NULL THEN
1816       FOR j IN 1..array_upper(search, 1) LOOP
1817         FOR k IN 1..array_upper(location.name, 1) LOOP
1818           IF (found > location.rank_address AND location.name[k].key = search[j] AND location.name[k].value != '') AND NOT result && ARRAY[trim(location.name[k].value)] AND (for_postcode IS NULL OR location.name[k].value ilike for_postcode||'%') THEN
1819             result[(100 - location.rank_address)] := trim(location.name[k].value);
1820             found := location.rank_address;
1821           END IF;
1822         END LOOP;
1823       END LOOP;
1824     END IF;
1825   END LOOP;
1826
1827   RETURN array_to_string(result,', ');
1828 END;
1829 $$
1830 LANGUAGE plpgsql;
1831
1832 CREATE OR REPLACE FUNCTION get_address_by_language(for_place_id INTEGER, languagepref TEXT[]) RETURNS TEXT
1833   AS $$
1834 DECLARE
1835   result TEXT[];
1836   search TEXT[];
1837   found INTEGER;
1838   location RECORD;
1839   searchcountrycode varchar(2);
1840   searchhousenumber TEXT;
1841   searchrankaddress INTEGER;
1842 BEGIN
1843
1844   found := 1000;
1845   search := languagepref;
1846   result := '{}';
1847
1848   select country_code,housenumber,rank_address from placex where place_id = for_place_id into searchcountrycode,searchhousenumber,searchrankaddress;
1849
1850   FOR location IN 
1851     select CASE WHEN address_place_id = for_place_id AND rank_address = 0 THEN 100 ELSE rank_address END as rank_address,
1852       CASE WHEN type = 'postcode' THEN 'name' => postcode ELSE name END as name,
1853       distance,length(name::text) as namelength 
1854       from place_addressline join placex on (address_place_id = placex.place_id) 
1855       where place_addressline.place_id = for_place_id and ((rank_address > 0 AND rank_address < searchrankaddress) OR address_place_id = for_place_id)
1856       and (placex.country_code IS NULL OR searchcountrycode IS NULL OR placex.country_code = searchcountrycode OR rank_address < 4)
1857       order by rank_address desc,fromarea desc,distance asc,rank_search desc,namelength desc
1858   LOOP
1859     IF array_upper(search, 1) IS NOT NULL AND location.name IS NOT NULL THEN
1860       FOR j IN 1..array_upper(search, 1) LOOP
1861         IF (found > location.rank_address AND location.name ? search[j] AND location.name -> search[j] != ''
1862             AND NOT result && ARRAY[location.name -> search[j]]) THEN
1863           result[(100 - location.rank_address)] := trim(location.name -> search[j]);
1864           found := location.rank_address;
1865         END IF;
1866       END LOOP;
1867     END IF;
1868   END LOOP;
1869
1870   IF searchhousenumber IS NOT NULL AND COALESCE(result[(100 - 28)],'') != searchhousenumber THEN
1871     IF result[(100 - 28)] IS NOT NULL THEN
1872       result[(100 - 29)] := result[(100 - 28)];
1873     END IF;
1874     result[(100 - 28)] := searchhousenumber;
1875   END IF;
1876
1877   -- No country polygon - add it from the country_code
1878   IF found > 4 THEN
1879     select get_name_by_language(country_name.name,languagepref) as name from placex join country_name using (country_code) 
1880       where place_id = for_place_id limit 1 INTO location;
1881     IF location IS NOT NULL THEN
1882       result[(100 - 4)] := trim(location.name);
1883     END IF;
1884   END IF;
1885
1886   RETURN array_to_string(result,', ');
1887 END;
1888 $$
1889 LANGUAGE plpgsql;
1890
1891 CREATE OR REPLACE FUNCTION get_addressdata_by_language(for_place_id INTEGER, languagepref TEXT[]) RETURNS TEXT[]
1892   AS $$
1893 DECLARE
1894   result TEXT[];
1895   search TEXT[];
1896   found INTEGER;
1897   location RECORD;
1898   searchcountrycode varchar(2);
1899   searchhousenumber TEXT;
1900 BEGIN
1901
1902   found := 1000;
1903   search := languagepref;
1904   result := '{}';
1905
1906   UPDATE placex set indexed_status = 0 where indexed_status > 0 and place_id = for_place_id;
1907
1908   select country_code,housenumber from placex where place_id = for_place_id into searchcountrycode,searchhousenumber;
1909
1910   FOR location IN 
1911     select CASE WHEN address_place_id = for_place_id AND rank_address = 0 THEN 100 ELSE rank_address END as rank_address,
1912       name,distance,length(name::text) as namelength 
1913       from place_addressline join placex on (address_place_id = placex.place_id) 
1914       where place_addressline.place_id = for_place_id and (rank_address > 0 OR address_place_id = for_place_id)
1915       and (placex.country_code IS NULL OR searchcountrycode IS NULL OR placex.country_code = searchcountrycode OR rank_address < 4)
1916       order by rank_address desc,fromarea desc,distance asc,rank_search desc,namelength desc
1917   LOOP
1918     IF array_upper(search, 1) IS NOT NULL AND array_upper(location.name, 1) IS NOT NULL THEN
1919       FOR j IN 1..array_upper(search, 1) LOOP
1920         FOR k IN 1..array_upper(location.name, 1) LOOP
1921           IF (found > location.rank_address AND location.name[k].key = search[j] AND location.name[k].value != '') AND NOT result && ARRAY[trim(location.name[k].value)] THEN
1922             result[(100 - location.rank_address)] := trim(location.name[k].value);
1923             found := location.rank_address;
1924           END IF;
1925         END LOOP;
1926       END LOOP;
1927     END IF;
1928   END LOOP;
1929
1930   IF searchhousenumber IS NOT NULL AND result[(100 - 28)] IS NULL THEN
1931     result[(100 - 28)] := searchhousenumber;
1932   END IF;
1933
1934   -- No country polygon - add it from the country_code
1935   IF found > 4 THEN
1936     select get_name_by_language(country_name.name,languagepref) as name from placex join country_name using (country_code) 
1937       where place_id = for_place_id limit 1 INTO location;
1938     IF location IS NOT NULL THEN
1939       result[(100 - 4)] := trim(location.name);
1940     END IF;
1941   END IF;
1942
1943   RETURN result;
1944 END;
1945 $$
1946 LANGUAGE plpgsql;
1947
1948 CREATE OR REPLACE FUNCTION get_place_boundingbox(search_place_id INTEGER) RETURNS place_boundingbox
1949   AS $$
1950 DECLARE
1951   result place_boundingbox;
1952   numfeatures integer;
1953 BEGIN
1954   select * from place_boundingbox into result where place_id = search_place_id;
1955   IF result.place_id IS NULL THEN
1956 -- remove  isaddress = true because if there is a matching polygon it always wins
1957     select count(*) from place_addressline where address_place_id = search_place_id into numfeatures;
1958     insert into place_boundingbox select place_id,
1959              ST_Y(ST_PointN(ExteriorRing(ST_Box2D(area)),4)),ST_Y(ST_PointN(ExteriorRing(ST_Box2D(area)),2)),
1960              ST_X(ST_PointN(ExteriorRing(ST_Box2D(area)),1)),ST_X(ST_PointN(ExteriorRing(ST_Box2D(area)),3)),
1961              numfeatures, ST_Area(area),
1962              area from location_area where place_id = search_place_id;
1963     select * from place_boundingbox into result where place_id = search_place_id;
1964   END IF;
1965   IF result.place_id IS NULL THEN
1966 -- TODO 0.0001
1967     insert into place_boundingbox select address_place_id,
1968              min(ST_Y(ST_Centroid(geometry))) as minlon,max(ST_Y(ST_Centroid(geometry))) as maxlon,
1969              min(ST_X(ST_Centroid(geometry))) as minlat,max(ST_X(ST_Centroid(geometry))) as maxlat,
1970              count(*), ST_Area(ST_Buffer(ST_Convexhull(ST_Collect(geometry)),0.0001)) as area,
1971              ST_Buffer(ST_Convexhull(ST_Collect(geometry)),0.0001) as boundary 
1972              from (select * from place_addressline where address_place_id = search_place_id order by cached_rank_address limit 4000) as place_addressline join placex using (place_id) 
1973              where address_place_id = search_place_id
1974 --               and (isaddress = true OR place_id = search_place_id)
1975                and (st_length(geometry) < 0.01 or place_id = search_place_id)
1976              group by address_place_id limit 1;
1977     select * from place_boundingbox into result where place_id = search_place_id;
1978   END IF;
1979   return result;
1980 END;
1981 $$
1982 LANGUAGE plpgsql;
1983
1984 -- don't do the operation if it would be slow
1985 CREATE OR REPLACE FUNCTION get_place_boundingbox_quick(search_place_id INTEGER) RETURNS place_boundingbox
1986   AS $$
1987 DECLARE
1988   result place_boundingbox;
1989   numfeatures integer;
1990   rank integer;
1991 BEGIN
1992   select * from place_boundingbox into result where place_id = search_place_id;
1993   IF result IS NULL AND rank > 14 THEN
1994     select count(*) from place_addressline where address_place_id = search_place_id and isaddress = true into numfeatures;
1995     insert into place_boundingbox select place_id,
1996              ST_Y(ST_PointN(ExteriorRing(ST_Box2D(area)),4)),ST_Y(ST_PointN(ExteriorRing(ST_Box2D(area)),2)),
1997              ST_X(ST_PointN(ExteriorRing(ST_Box2D(area)),1)),ST_X(ST_PointN(ExteriorRing(ST_Box2D(area)),3)),
1998              numfeatures, ST_Area(area),
1999              area from location_area where place_id = search_place_id;
2000     select * from place_boundingbox into result where place_id = search_place_id;
2001   END IF;
2002   IF result IS NULL THEN
2003     select rank_search from placex where place_id = search_place_id into rank;
2004     IF rank > 20 THEN
2005 -- TODO 0.0001
2006       insert into place_boundingbox select address_place_id,
2007              min(ST_Y(ST_Centroid(geometry))) as minlon,max(ST_Y(ST_Centroid(geometry))) as maxlon,
2008              min(ST_X(ST_Centroid(geometry))) as minlat,max(ST_X(ST_Centroid(geometry))) as maxlat,
2009              count(*), ST_Area(ST_Buffer(ST_Convexhull(ST_Collect(geometry)),0.0001)) as area,
2010              ST_Buffer(ST_Convexhull(ST_Collect(geometry)),0.0001) as boundary 
2011              from place_addressline join placex using (place_id) 
2012              where address_place_id = search_place_id 
2013                and (isaddress = true OR place_id = search_place_id)
2014                and (st_length(geometry) < 0.01 or place_id = search_place_id)
2015              group by address_place_id limit 1;
2016       select * from place_boundingbox into result where place_id = search_place_id;
2017     END IF;
2018   END IF;
2019   return result;
2020 END;
2021 $$
2022 LANGUAGE plpgsql;
2023
2024 CREATE OR REPLACE FUNCTION update_place(search_place_id INTEGER) RETURNS BOOLEAN
2025   AS $$
2026 DECLARE
2027   result place_boundingbox;
2028   numfeatures integer;
2029 BEGIN
2030   update placex set 
2031       name = place.name,
2032       housenumber = place.housenumber,
2033       street = place.street,
2034       isin = place.isin,
2035       postcode = place.postcode,
2036       country_code = place.country_code,
2037       street_place_id = null,
2038       indexed = false      
2039       from place
2040       where placex.place_id = search_place_id 
2041         and place.osm_type = placex.osm_type and place.osm_id = placex.osm_id
2042         and place.class = placex.class and place.type = placex.type;
2043   update placex set indexed = true where place_id = search_place_id and indexed = false;
2044   return true;
2045 END;
2046 $$
2047 LANGUAGE plpgsql;
2048
2049 CREATE OR REPLACE FUNCTION update_place(search_place_id INTEGER) RETURNS BOOLEAN
2050   AS $$
2051 DECLARE
2052   result place_boundingbox;
2053   numfeatures integer;
2054 BEGIN
2055   update placex set 
2056       name = place.name,
2057       housenumber = place.housenumber,
2058       street = place.street,
2059       isin = place.isin,
2060       postcode = place.postcode,
2061       country_code = place.country_code,
2062       street_place_id = null,
2063       indexed = false      
2064       from place
2065       where placex.place_id = search_place_id 
2066         and place.osm_type = placex.osm_type and place.osm_id = placex.osm_id
2067         and place.class = placex.class and place.type = placex.type;
2068   update placex set indexed = true where place_id = search_place_id and indexed = false;
2069   return true;
2070 END;
2071 $$
2072 LANGUAGE plpgsql;
2073
2074 CREATE OR REPLACE FUNCTION get_searchrank_label(rank INTEGER) RETURNS TEXT
2075   AS $$
2076 DECLARE
2077 BEGIN
2078   IF rank < 2 THEN
2079     RETURN 'Continent';
2080   ELSEIF rank < 4 THEN
2081     RETURN 'Sea';
2082   ELSEIF rank < 8 THEN
2083     RETURN 'Country';
2084   ELSEIF rank < 12 THEN
2085     RETURN 'State';
2086   ELSEIF rank < 16 THEN
2087     RETURN 'County';
2088   ELSEIF rank = 16 THEN
2089     RETURN 'City';
2090   ELSEIF rank = 17 THEN
2091     RETURN 'Town / Island';
2092   ELSEIF rank = 18 THEN
2093     RETURN 'Village / Hamlet';
2094   ELSEIF rank = 20 THEN
2095     RETURN 'Suburb';
2096   ELSEIF rank = 21 THEN
2097     RETURN 'Postcode Area';
2098   ELSEIF rank = 22 THEN
2099     RETURN 'Croft / Farm / Locality / Islet';
2100   ELSEIF rank = 23 THEN
2101     RETURN 'Postcode Area';
2102   ELSEIF rank = 25 THEN
2103     RETURN 'Postcode Point';
2104   ELSEIF rank = 26 THEN
2105     RETURN 'Street / Major Landmark';
2106   ELSEIF rank = 27 THEN
2107     RETURN 'Minory Street / Path';
2108   ELSEIF rank = 28 THEN
2109     RETURN 'House / Building';
2110   ELSE
2111     RETURN 'Other: '||rank;
2112   END IF;
2113   
2114 END;
2115 $$
2116 LANGUAGE plpgsql;
2117
2118 CREATE OR REPLACE FUNCTION get_addressrank_label(rank INTEGER) RETURNS TEXT
2119   AS $$
2120 DECLARE
2121 BEGIN
2122   IF rank = 0 THEN
2123     RETURN 'None';
2124   ELSEIF rank < 2 THEN
2125     RETURN 'Continent';
2126   ELSEIF rank < 4 THEN
2127     RETURN 'Sea';
2128   ELSEIF rank = 5 THEN
2129     RETURN 'Postcode';
2130   ELSEIF rank < 8 THEN
2131     RETURN 'Country';
2132   ELSEIF rank < 12 THEN
2133     RETURN 'State';
2134   ELSEIF rank < 16 THEN
2135     RETURN 'County';
2136   ELSEIF rank = 16 THEN
2137     RETURN 'City';
2138   ELSEIF rank = 17 THEN
2139     RETURN 'Town / Village / Hamlet';
2140   ELSEIF rank = 20 THEN
2141     RETURN 'Suburb';
2142   ELSEIF rank = 21 THEN
2143     RETURN 'Postcode Area';
2144   ELSEIF rank = 22 THEN
2145     RETURN 'Croft / Farm / Locality / Islet';
2146   ELSEIF rank = 23 THEN
2147     RETURN 'Postcode Area';
2148   ELSEIF rank = 25 THEN
2149     RETURN 'Postcode Point';
2150   ELSEIF rank = 26 THEN
2151     RETURN 'Street / Major Landmark';
2152   ELSEIF rank = 27 THEN
2153     RETURN 'Minory Street / Path';
2154   ELSEIF rank = 28 THEN
2155     RETURN 'House / Building';
2156   ELSE
2157     RETURN 'Other: '||rank;
2158   END IF;
2159   
2160 END;
2161 $$
2162 LANGUAGE plpgsql;
2163
2164 CREATE OR REPLACE FUNCTION get_word_suggestion(srcword TEXT) RETURNS TEXT
2165   AS $$
2166 DECLARE
2167   trigramtoken TEXT;
2168   result TEXT;
2169 BEGIN
2170
2171   trigramtoken := regexp_replace(make_standard_name(srcword),E'([^0-9])\\1+',E'\\1','g');
2172   SELECT word FROM word WHERE word_trigram like ' %' and word_trigram % trigramtoken ORDER BY similarity(word_trigram, trigramtoken) DESC, word limit 1 into result;
2173
2174   return result;
2175 END;
2176 $$
2177 LANGUAGE plpgsql;
2178
2179 CREATE OR REPLACE FUNCTION get_word_suggestions(srcword TEXT) RETURNS TEXT[]
2180   AS $$
2181 DECLARE
2182   trigramtoken TEXT;
2183   result TEXT[];
2184   r RECORD;
2185 BEGIN
2186
2187   trigramtoken := regexp_replace(make_standard_name(srcword),E'([^0-9])\\1+',E'\\1','g');
2188
2189   FOR r IN SELECT word,similarity(word_trigram, trigramtoken) as score FROM word 
2190     WHERE word_trigram like ' %' and word_trigram % trigramtoken ORDER BY similarity(word_trigram, trigramtoken) DESC, word limit 4
2191   LOOP
2192     result[coalesce(array_upper(result,1)+1,1)] := r.word;
2193   END LOOP;
2194
2195   return result;
2196 END;
2197 $$
2198 LANGUAGE plpgsql;
2199
2200 CREATE AGGREGATE array_agg(INT[])
2201 (
2202     sfunc = array_cat,
2203     stype = INT[],
2204     initcond = '{}'
2205 );
2206
2207