]> git.openstreetmap.org Git - nominatim.git/blob - lib/Geocode.php
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / lib / Geocode.php
1 <?php
2
3 namespace Nominatim;
4
5 require_once(CONST_BasePath.'/lib/PlaceLookup.php');
6 require_once(CONST_BasePath.'/lib/ReverseGeocode.php');
7
8 class Geocode
9 {
10     protected $oDB;
11
12     protected $aLangPrefOrder = array();
13
14     protected $bIncludeAddressDetails = false;
15     protected $bIncludeExtraTags = false;
16     protected $bIncludeNameDetails = false;
17
18     protected $bIncludePolygonAsPoints = false;
19     protected $bIncludePolygonAsText = false;
20     protected $bIncludePolygonAsGeoJSON = false;
21     protected $bIncludePolygonAsKML = false;
22     protected $bIncludePolygonAsSVG = false;
23     protected $fPolygonSimplificationThreshold = 0.0;
24
25     protected $aExcludePlaceIDs = array();
26     protected $bDeDupe = true;
27     protected $bReverseInPlan = true;
28
29     protected $iLimit = 20;
30     protected $iFinalLimit = 10;
31     protected $iOffset = 0;
32     protected $bFallback = false;
33
34     protected $aCountryCodes = false;
35     protected $aNearPoint = false;
36
37     protected $bBoundedSearch = false;
38     protected $aViewBox = false;
39     protected $sViewboxCentreSQL = false;
40     protected $sViewboxSmallSQL = false;
41     protected $sViewboxLargeSQL = false;
42
43     protected $iMaxRank = 20;
44     protected $iMinAddressRank = 0;
45     protected $iMaxAddressRank = 30;
46     protected $aAddressRankList = array();
47     protected $exactMatchCache = array();
48
49     protected $sAllowedTypesSQLList = false;
50
51     protected $sQuery = false;
52     protected $aStructuredQuery = false;
53
54
55     public function __construct(&$oDB)
56     {
57         $this->oDB =& $oDB;
58     }
59
60     public function setReverseInPlan($bReverse)
61     {
62         $this->bReverseInPlan = $bReverse;
63     }
64
65     public function setLanguagePreference($aLangPref)
66     {
67         $this->aLangPrefOrder = $aLangPref;
68     }
69
70     public function getIncludeAddressDetails()
71     {
72         return $this->bIncludeAddressDetails;
73     }
74
75     public function getIncludeExtraTags()
76     {
77         return $this->bIncludeExtraTags;
78     }
79
80     public function getIncludeNameDetails()
81     {
82         return $this->bIncludeNameDetails;
83     }
84
85     public function setIncludePolygonAsPoints($b = true)
86     {
87         $this->bIncludePolygonAsPoints = $b;
88     }
89
90     public function setIncludePolygonAsText($b = true)
91     {
92         $this->bIncludePolygonAsText = $b;
93     }
94
95     public function setIncludePolygonAsGeoJSON($b = true)
96     {
97         $this->bIncludePolygonAsGeoJSON = $b;
98     }
99
100     public function setIncludePolygonAsKML($b = true)
101     {
102         $this->bIncludePolygonAsKML = $b;
103     }
104
105     public function setIncludePolygonAsSVG($b = true)
106     {
107         $this->bIncludePolygonAsSVG = $b;
108     }
109
110     public function setPolygonSimplificationThreshold($f)
111     {
112         $this->fPolygonSimplificationThreshold = $f;
113     }
114
115     public function setLimit($iLimit = 10)
116     {
117         if ($iLimit > 50) $iLimit = 50;
118         if ($iLimit < 1) $iLimit = 1;
119
120         $this->iFinalLimit = $iLimit;
121         $this->iLimit = $iLimit + min($iLimit, 10);
122     }
123
124     public function getExcludedPlaceIDs()
125     {
126         return $this->aExcludePlaceIDs;
127     }
128
129
130     public function getCountryCodes()
131     {
132         return $this->aCountryCodes;
133     }
134
135     public function getViewBoxString()
136     {
137         if (!$this->aViewBox) return null;
138         return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
139     }
140
141     public function setFeatureType($sFeatureType)
142     {
143         switch ($sFeatureType) {
144             case 'country':
145                 $this->setRankRange(4, 4);
146                 break;
147             case 'state':
148                 $this->setRankRange(8, 8);
149                 break;
150             case 'city':
151                 $this->setRankRange(14, 16);
152                 break;
153             case 'settlement':
154                 $this->setRankRange(8, 20);
155                 break;
156         }
157     }
158
159     public function setRankRange($iMin, $iMax)
160     {
161         $this->iMinAddressRank = $iMin;
162         $this->iMaxAddressRank = $iMax;
163     }
164
165     public function setRoute($aRoutePoints, $fRouteWidth)
166     {
167         $this->aViewBox = false;
168
169         $this->sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
170         $sSep = '';
171         foreach ($aRoutePoints as $aPoint) {
172             $fPoint = (float)$aPoint;
173             $this->sViewboxCentreSQL .= $sSep.$fPoint;
174             $sSep = ($sSep == ' ') ? ',' : ' ';
175         }
176         $this->sViewboxCentreSQL .= ")'::geometry,4326)";
177
178         $this->sViewboxSmallSQL = 'ST_BUFFER('.$this->sViewboxCentreSQL;
179         $this->sViewboxSmallSQL .= ','.($fRouteWidth/69).')';
180
181         $this->sViewboxLargeSQL = 'ST_BUFFER('.$this->sViewboxCentreSQL;
182         $this->sViewboxLargeSQL .= ','.($fRouteWidth/30).')';
183     }
184
185     public function setViewbox($aViewbox)
186     {
187         $this->aViewBox = array_map('floatval', $aViewbox);
188
189         $this->aViewBox[0] = max(-180.0, min(180, $this->aViewBox[0]));
190         $this->aViewBox[1] = max(-90.0, min(90, $this->aViewBox[1]));
191         $this->aViewBox[2] = max(-180.0, min(180, $this->aViewBox[2]));
192         $this->aViewBox[3] = max(-90.0, min(90, $this->aViewBox[3]));
193
194         if (abs($this->aViewBox[0] - $this->aViewBox[2]) < 0.000000001
195             || abs($this->aViewBox[1] - $this->aViewBox[3]) < 0.000000001
196         ) {
197             userError("Bad parameter 'viewbox'. Not a box.");
198         }
199
200         $fHeight = $this->aViewBox[0] - $this->aViewBox[2];
201         $fWidth = $this->aViewBox[1] - $this->aViewBox[3];
202         $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
203         $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
204         $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
205         $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
206
207         $this->sViewboxCentreSQL = false;
208         $this->sViewboxSmallSQL = sprintf(
209             'ST_SetSRID(ST_MakeBox2D(ST_Point(%F,%F),ST_Point(%F,%F)),4326)',
210             $this->aViewBox[0],
211             $this->aViewBox[1],
212             $this->aViewBox[2],
213             $this->aViewBox[3]
214         );
215         $this->sViewboxLargeSQL = sprintf(
216             'ST_SetSRID(ST_MakeBox2D(ST_Point(%F,%F),ST_Point(%F,%F)),4326)',
217             $aBigViewBox[0],
218             $aBigViewBox[1],
219             $aBigViewBox[2],
220             $aBigViewBox[3]
221         );
222     }
223
224     public function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
225     {
226         $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
227     }
228
229     public function setQuery($sQueryString)
230     {
231         $this->sQuery = $sQueryString;
232         $this->aStructuredQuery = false;
233     }
234
235     public function getQueryString()
236     {
237         return $this->sQuery;
238     }
239
240
241     public function loadParamArray($oParams)
242     {
243         $this->bIncludeAddressDetails
244          = $oParams->getBool('addressdetails', $this->bIncludeAddressDetails);
245         $this->bIncludeExtraTags
246          = $oParams->getBool('extratags', $this->bIncludeExtraTags);
247         $this->bIncludeNameDetails
248          = $oParams->getBool('namedetails', $this->bIncludeNameDetails);
249
250         $this->bBoundedSearch = $oParams->getBool('bounded', $this->bBoundedSearch);
251         $this->bDeDupe = $oParams->getBool('dedupe', $this->bDeDupe);
252
253         $this->setLimit($oParams->getInt('limit', $this->iFinalLimit));
254         $this->iOffset = $oParams->getInt('offset', $this->iOffset);
255
256         $this->bFallback = $oParams->getBool('fallback', $this->bFallback);
257
258         // List of excluded Place IDs - used for more acurate pageing
259         $sExcluded = $oParams->getStringList('exclude_place_ids');
260         if ($sExcluded) {
261             foreach ($sExcluded as $iExcludedPlaceID) {
262                 $iExcludedPlaceID = (int)$iExcludedPlaceID;
263                 if ($iExcludedPlaceID)
264                     $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID;
265             }
266
267             if (isset($aExcludePlaceIDs))
268                 $this->aExcludePlaceIDs = $aExcludePlaceIDs;
269         }
270
271         // Only certain ranks of feature
272         $sFeatureType = $oParams->getString('featureType');
273         if (!$sFeatureType) $sFeatureType = $oParams->getString('featuretype');
274         if ($sFeatureType) $this->setFeatureType($sFeatureType);
275
276         // Country code list
277         $sCountries = $oParams->getStringList('countrycodes');
278         if ($sCountries) {
279             foreach ($sCountries as $sCountryCode) {
280                 if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode)) {
281                     $aCountries[] = strtolower($sCountryCode);
282                 }
283             }
284             if (isset($aCountries))
285                 $this->aCountryCodes = $aCountries;
286         }
287
288         $aViewbox = $oParams->getStringList('viewboxlbrt');
289         if ($aViewbox) {
290             if (count($aViewbox) != 4) {
291                 userError("Bad parmater 'viewbox'. Expected 4 coordinates.");
292             }
293             $this->setViewbox($aViewbox);
294         } else {
295             $aViewbox = $oParams->getStringList('viewbox');
296             if ($aViewbox) {
297                 if (count($aViewbox) != 4) {
298                     userError("Bad parmater 'viewbox'. Expected 4 coordinates.");
299                 }
300                 $this->setViewBox(array(
301                                    $aViewbox[0],
302                                    $aViewbox[3],
303                                    $aViewbox[2],
304                                    $aViewbox[1]
305                                   ));
306             } else {
307                 $aRoute = $oParams->getStringList('route');
308                 $fRouteWidth = $oParams->getFloat('routewidth');
309                 if ($aRoute && $fRouteWidth) {
310                     $this->setRoute($aRoute, $fRouteWidth);
311                 }
312             }
313         }
314     }
315
316     public function setQueryFromParams($oParams)
317     {
318         // Search query
319         $sQuery = $oParams->getString('q');
320         if (!$sQuery) {
321             $this->setStructuredQuery(
322                 $oParams->getString('amenity'),
323                 $oParams->getString('street'),
324                 $oParams->getString('city'),
325                 $oParams->getString('county'),
326                 $oParams->getString('state'),
327                 $oParams->getString('country'),
328                 $oParams->getString('postalcode')
329             );
330             $this->setReverseInPlan(false);
331         } else {
332             $this->setQuery($sQuery);
333         }
334     }
335
336     public function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
337     {
338         $sValue = trim($sValue);
339         if (!$sValue) return false;
340         $this->aStructuredQuery[$sKey] = $sValue;
341         if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30) {
342             $this->iMinAddressRank = $iNewMinAddressRank;
343             $this->iMaxAddressRank = $iNewMaxAddressRank;
344         }
345         if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
346         return true;
347     }
348
349     public function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
350     {
351         $this->sQuery = false;
352
353         // Reset
354         $this->iMinAddressRank = 0;
355         $this->iMaxAddressRank = 30;
356         $this->aAddressRankList = array();
357
358         $this->aStructuredQuery = array();
359         $this->sAllowedTypesSQLList = '';
360
361         $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
362         $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
363         $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
364         $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
365         $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
366         $this->loadStructuredAddressElement($sPostalCode, 'postalcode', 5, 11, array(5, 11));
367         $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
368
369         if (sizeof($this->aStructuredQuery) > 0) {
370             $this->sQuery = join(', ', $this->aStructuredQuery);
371             if ($this->iMaxAddressRank < 30) {
372                 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
373             }
374         }
375     }
376
377     public function fallbackStructuredQuery()
378     {
379         if (!$this->aStructuredQuery) return false;
380
381         $aParams = $this->aStructuredQuery;
382
383         if (sizeof($aParams) == 1) return false;
384
385         $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
386
387         foreach ($aOrderToFallback as $sType) {
388             if (isset($aParams[$sType])) {
389                 unset($aParams[$sType]);
390                 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
391                 return true;
392             }
393         }
394
395         return false;
396     }
397
398     public function getDetails($aPlaceIDs)
399     {
400         //$aPlaceIDs is an array with key: placeID and value: tiger-housenumber, if found, else -1
401         if (sizeof($aPlaceIDs) == 0) return array();
402
403         $sLanguagePrefArraySQL = "ARRAY[".join(',', array_map("getDBQuoted", $this->aLangPrefOrder))."]";
404
405         // Get the details for display (is this a redundant extra step?)
406         $sPlaceIDs = join(',', array_keys($aPlaceIDs));
407
408         $sImportanceSQL = '';
409         if ($this->sViewboxSmallSQL) $sImportanceSQL .= " CASE WHEN ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
410         if ($this->sViewboxLargeSQL) $sImportanceSQL .= " CASE WHEN ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
411
412         $sSQL  = "SELECT ";
413         $sSQL .= "    osm_type,";
414         $sSQL .= "    osm_id,";
415         $sSQL .= "    class,";
416         $sSQL .= "    type,";
417         $sSQL .= "    admin_level,";
418         $sSQL .= "    rank_search,";
419         $sSQL .= "    rank_address,";
420         $sSQL .= "    min(place_id) AS place_id, ";
421         $sSQL .= "    min(parent_place_id) AS parent_place_id, ";
422         $sSQL .= "    calculated_country_code AS country_code, ";
423         $sSQL .= "    get_address_by_language(place_id, -1, $sLanguagePrefArraySQL) AS langaddress,";
424         $sSQL .= "    get_name_by_language(name, $sLanguagePrefArraySQL) AS placename,";
425         $sSQL .= "    get_name_by_language(name, ARRAY['ref']) AS ref,";
426         if ($this->bIncludeExtraTags) $sSQL .= "hstore_to_json(extratags)::text AS extra,";
427         if ($this->bIncludeNameDetails) $sSQL .= "hstore_to_json(name)::text AS names,";
428         $sSQL .= "    avg(ST_X(centroid)) AS lon, ";
429         $sSQL .= "    avg(ST_Y(centroid)) AS lat, ";
430         $sSQL .= "    ".$sImportanceSQL."COALESCE(importance,0.75-(rank_search::float/40)) AS importance, ";
431         $sSQL .= "    ( ";
432         $sSQL .= "       SELECT max(p.importance*(p.rank_address+2))";
433         $sSQL .= "       FROM ";
434         $sSQL .= "         place_addressline s, ";
435         $sSQL .= "         placex p";
436         $sSQL .= "       WHERE s.place_id = min(CASE WHEN placex.rank_search < 28 THEN placex.place_id ELSE placex.parent_place_id END)";
437         $sSQL .= "         AND p.place_id = s.address_place_id ";
438         $sSQL .= "         AND s.isaddress ";
439         $sSQL .= "         AND p.importance is not null ";
440         $sSQL .= "    ) AS addressimportance, ";
441         $sSQL .= "    (extratags->'place') AS extra_place ";
442         $sSQL .= " FROM placex";
443         $sSQL .= " WHERE place_id in ($sPlaceIDs) ";
444         $sSQL .= "   AND (";
445         $sSQL .= "            placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
446         if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) {
447             $sSQL .= "        OR (extratags->'place') = 'city'";
448         }
449         if ($this->aAddressRankList) {
450             $sSQL .= "        OR placex.rank_address in (".join(',', $this->aAddressRankList).")";
451         }
452         $sSQL .= "       ) ";
453         if ($this->sAllowedTypesSQLList) {
454             $sSQL .= "AND placex.class in $this->sAllowedTypesSQLList ";
455         }
456         $sSQL .= "    AND linked_place_id is null ";
457         $sSQL .= " GROUP BY ";
458         $sSQL .= "     osm_type, ";
459         $sSQL .= "     osm_id, ";
460         $sSQL .= "     class, ";
461         $sSQL .= "     type, ";
462         $sSQL .= "     admin_level, ";
463         $sSQL .= "     rank_search, ";
464         $sSQL .= "     rank_address, ";
465         $sSQL .= "     calculated_country_code, ";
466         $sSQL .= "     importance, ";
467         if (!$this->bDeDupe) $sSQL .= "place_id,";
468         $sSQL .= "     langaddress, ";
469         $sSQL .= "     placename, ";
470         $sSQL .= "     ref, ";
471         if ($this->bIncludeExtraTags) $sSQL .= "extratags, ";
472         if ($this->bIncludeNameDetails) $sSQL .= "name, ";
473         $sSQL .= "     extratags->'place' ";
474
475         if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank) {
476             // only Tiger housenumbers and interpolation lines need to be interpolated, because they are saved as lines
477             // with start- and endnumber, the common osm housenumbers are usually saved as points
478             $sHousenumbers = "";
479             $i = 0;
480             $length = count($aPlaceIDs);
481             foreach ($aPlaceIDs as $placeID => $housenumber) {
482                 $i++;
483                 $sHousenumbers .= "(".$placeID.", ".$housenumber.")";
484                 if ($i<$length) $sHousenumbers .= ", ";
485             }
486
487             if (CONST_Use_US_Tiger_Data) {
488                 // Tiger search only if a housenumber was searched and if it was found (i.e. aPlaceIDs[placeID] = housenumber != -1) (realized through a join)
489                 $sSQL .= " union";
490                 $sSQL .= " SELECT ";
491                 $sSQL .= "     'T' AS osm_type, ";
492                 $sSQL .= "     (SELECT osm_id from placex p WHERE p.place_id=min(blub.parent_place_id)) as osm_id, ";
493                 $sSQL .= "     'place' AS class, ";
494                 $sSQL .= "     'house' AS type, ";
495                 $sSQL .= "     null AS admin_level, ";
496                 $sSQL .= "     30 AS rank_search, ";
497                 $sSQL .= "     30 AS rank_address, ";
498                 $sSQL .= "     min(place_id) AS place_id, ";
499                 $sSQL .= "     min(parent_place_id) AS parent_place_id, ";
500                 $sSQL .= "     'us' AS country_code, ";
501                 $sSQL .= "     get_address_by_language(place_id, housenumber_for_place, $sLanguagePrefArraySQL) AS langaddress,";
502                 $sSQL .= "     null AS placename, ";
503                 $sSQL .= "     null AS ref, ";
504                 if ($this->bIncludeExtraTags) $sSQL .= "null AS extra,";
505                 if ($this->bIncludeNameDetails) $sSQL .= "null AS names,";
506                 $sSQL .= "     avg(st_x(centroid)) AS lon, ";
507                 $sSQL .= "     avg(st_y(centroid)) AS lat,";
508                 $sSQL .= "     ".$sImportanceSQL."-1.15 AS importance, ";
509                 $sSQL .= "     (";
510                 $sSQL .= "        SELECT max(p.importance*(p.rank_address+2))";
511                 $sSQL .= "        FROM ";
512                 $sSQL .= "          place_addressline s, ";
513                 $sSQL .= "          placex p";
514                 $sSQL .= "        WHERE s.place_id = min(blub.parent_place_id)";
515                 $sSQL .= "          AND p.place_id = s.address_place_id ";
516                 $sSQL .= "          AND s.isaddress";
517                 $sSQL .= "          AND p.importance is not null";
518                 $sSQL .= "     ) AS addressimportance, ";
519                 $sSQL .= "     null AS extra_place ";
520                 $sSQL .= " FROM (";
521                 $sSQL .= "     SELECT place_id, ";    // interpolate the Tiger housenumbers here
522                 $sSQL .= "         ST_LineInterpolatePoint(linegeo, (housenumber_for_place-startnumber::float)/(endnumber-startnumber)::float) AS centroid, ";
523                 $sSQL .= "         parent_place_id, ";
524                 $sSQL .= "         housenumber_for_place";
525                 $sSQL .= "     FROM (";
526                 $sSQL .= "            location_property_tiger ";
527                 $sSQL .= "            JOIN (values ".$sHousenumbers.") AS housenumbers(place_id, housenumber_for_place) USING(place_id)) ";
528                 $sSQL .= "     WHERE ";
529                 $sSQL .= "         housenumber_for_place>=0";
530                 $sSQL .= "         AND 30 between $this->iMinAddressRank AND $this->iMaxAddressRank";
531                 $sSQL .= " ) AS blub"; //postgres wants an alias here
532                 $sSQL .= " GROUP BY";
533                 $sSQL .= "      place_id, ";
534                 $sSQL .= "      housenumber_for_place"; //is this group by really needed?, place_id + housenumber (in combination) are unique
535                 if (!$this->bDeDupe) $sSQL .= ", place_id ";
536             }
537             // osmline
538             // interpolation line search only if a housenumber was searched and if it was found (i.e. aPlaceIDs[placeID] = housenumber != -1) (realized through a join)
539             $sSQL .= " UNION ";
540             $sSQL .= "SELECT ";
541             $sSQL .= "  'W' AS osm_type, ";
542             $sSQL .= "  osm_id, ";
543             $sSQL .= "  'place' AS class, ";
544             $sSQL .= "  'house' AS type, ";
545             $sSQL .= "  null AS admin_level, ";
546             $sSQL .= "  30 AS rank_search, ";
547             $sSQL .= "  30 AS rank_address, ";
548             $sSQL .= "  min(place_id) as place_id, ";
549             $sSQL .= "  min(parent_place_id) AS parent_place_id, ";
550             $sSQL .= "  calculated_country_code AS country_code, ";
551             $sSQL .= "  get_address_by_language(place_id, housenumber_for_place, $sLanguagePrefArraySQL) AS langaddress, ";
552             $sSQL .= "  null AS placename, ";
553             $sSQL .= "  null AS ref, ";
554             if ($this->bIncludeExtraTags) $sSQL .= "null AS extra, ";
555             if ($this->bIncludeNameDetails) $sSQL .= "null AS names, ";
556             $sSQL .= "  AVG(st_x(centroid)) AS lon, ";
557             $sSQL .= "  AVG(st_y(centroid)) AS lat, ";
558             $sSQL .= "  ".$sImportanceSQL."-0.1 AS importance, ";  // slightly smaller than the importance for normal houses with rank 30, which is 0
559             $sSQL .= "  (";
560             $sSQL .= "     SELECT ";
561             $sSQL .= "       MAX(p.importance*(p.rank_address+2)) ";
562             $sSQL .= "     FROM";
563             $sSQL .= "       place_addressline s, ";
564             $sSQL .= "       placex p";
565             $sSQL .= "     WHERE s.place_id = min(blub.parent_place_id) ";
566             $sSQL .= "       AND p.place_id = s.address_place_id ";
567             $sSQL .= "       AND s.isaddress ";
568             $sSQL .= "       AND p.importance is not null";
569             $sSQL .= "  ) AS addressimportance,";
570             $sSQL .= "  null AS extra_place ";
571             $sSQL .= "  FROM (";
572             $sSQL .= "     SELECT ";
573             $sSQL .= "         osm_id, ";
574             $sSQL .= "         place_id, ";
575             $sSQL .= "         calculated_country_code, ";
576             $sSQL .= "         CASE ";             // interpolate the housenumbers here
577             $sSQL .= "           WHEN startnumber != endnumber ";
578             $sSQL .= "           THEN ST_LineInterpolatePoint(linegeo, (housenumber_for_place-startnumber::float)/(endnumber-startnumber)::float) ";
579             $sSQL .= "           ELSE ST_LineInterpolatePoint(linegeo, 0.5) ";
580             $sSQL .= "         END as centroid, ";
581             $sSQL .= "         parent_place_id, ";
582             $sSQL .= "         housenumber_for_place ";
583             $sSQL .= "     FROM (";
584             $sSQL .= "            location_property_osmline ";
585             $sSQL .= "            JOIN (values ".$sHousenumbers.") AS housenumbers(place_id, housenumber_for_place) USING(place_id)";
586             $sSQL .= "          ) ";
587             $sSQL .= "     WHERE housenumber_for_place>=0 ";
588             $sSQL .= "       AND 30 between $this->iMinAddressRank AND $this->iMaxAddressRank";
589             $sSQL .= "  ) as blub"; //postgres wants an alias here
590             $sSQL .= "  GROUP BY ";
591             $sSQL .= "    osm_id, ";
592             $sSQL .= "    place_id, ";
593             $sSQL .= "    housenumber_for_place, ";
594             $sSQL .= "    calculated_country_code "; //is this group by really needed?, place_id + housenumber (in combination) are unique
595             if (!$this->bDeDupe) $sSQL .= ", place_id ";
596
597             if (CONST_Use_Aux_Location_data) {
598                 $sSQL .= " UNION ";
599                 $sSQL .= "  SELECT ";
600                 $sSQL .= "     'L' AS osm_type, ";
601                 $sSQL .= "     place_id AS osm_id, ";
602                 $sSQL .= "     'place' AS class,";
603                 $sSQL .= "     'house' AS type, ";
604                 $sSQL .= "     null AS admin_level, ";
605                 $sSQL .= "     0 AS rank_search,";
606                 $sSQL .= "     0 AS rank_address, ";
607                 $sSQL .= "     min(place_id) AS place_id,";
608                 $sSQL .= "     min(parent_place_id) AS parent_place_id, ";
609                 $sSQL .= "     'us' AS country_code, ";
610                 $sSQL .= "     get_address_by_language(place_id, -1, $sLanguagePrefArraySQL) AS langaddress, ";
611                 $sSQL .= "     null AS placename, ";
612                 $sSQL .= "     null AS ref, ";
613                 if ($this->bIncludeExtraTags) $sSQL .= "null AS extra, ";
614                 if ($this->bIncludeNameDetails) $sSQL .= "null AS names, ";
615                 $sSQL .= "     avg(ST_X(centroid)) AS lon, ";
616                 $sSQL .= "     avg(ST_Y(centroid)) AS lat, ";
617                 $sSQL .= "     ".$sImportanceSQL."-1.10 AS importance, ";
618                 $sSQL .= "     ( ";
619                 $sSQL .= "       SELECT max(p.importance*(p.rank_address+2))";
620                 $sSQL .= "       FROM ";
621                 $sSQL .= "          place_addressline s, ";
622                 $sSQL .= "          placex p";
623                 $sSQL .= "       WHERE s.place_id = min(location_property_aux.parent_place_id)";
624                 $sSQL .= "         AND p.place_id = s.address_place_id ";
625                 $sSQL .= "         AND s.isaddress";
626                 $sSQL .= "         AND p.importance is not null";
627                 $sSQL .= "     ) AS addressimportance, ";
628                 $sSQL .= "     null AS extra_place ";
629                 $sSQL .= "  FROM location_property_aux ";
630                 $sSQL .= "  WHERE place_id in ($sPlaceIDs) ";
631                 $sSQL .= "    AND 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
632                 $sSQL .= "  GROUP BY ";
633                 $sSQL .= "     place_id, ";
634                 if (!$this->bDeDupe) $sSQL .= "place_id, ";
635                 $sSQL .= "     get_address_by_language(place_id, -1, $sLanguagePrefArraySQL) ";
636             }
637         }
638
639         $sSQL .= " order by importance desc";
640         if (CONST_Debug) {
641             echo "<hr>";
642             var_dump($sSQL);
643         }
644         $aSearchResults = chksql(
645             $this->oDB->getAll($sSQL),
646             "Could not get details for place."
647         );
648
649         return $aSearchResults;
650     }
651
652     public function getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases)
653     {
654         /*
655              Calculate all searches using aValidTokens i.e.
656              'Wodsworth Road, Sheffield' =>
657
658              Phrase Wordset
659              0      0       (wodsworth road)
660              0      1       (wodsworth)(road)
661              1      0       (sheffield)
662
663              Score how good the search is so they can be ordered
664          */
665         foreach ($aPhrases as $iPhrase => $sPhrase) {
666             $aNewPhraseSearches = array();
667             if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
668             else $sPhraseType = '';
669
670             foreach ($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset) {
671                 // Too many permutations - too expensive
672                 if ($iWordSet > 120) break;
673
674                 $aWordsetSearches = $aSearches;
675
676                 // Add all words from this wordset
677                 foreach ($aWordset as $iToken => $sToken) {
678                     //echo "<br><b>$sToken</b>";
679                     $aNewWordsetSearches = array();
680
681                     foreach ($aWordsetSearches as $aCurrentSearch) {
682                         //echo "<i>";
683                         //var_dump($aCurrentSearch);
684                         //echo "</i>";
685
686                         // If the token is valid
687                         if (isset($aValidTokens[' '.$sToken])) {
688                             foreach ($aValidTokens[' '.$sToken] as $aSearchTerm) {
689                                 $aSearch = $aCurrentSearch;
690                                 $aSearch['iSearchRank']++;
691                                 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0') {
692                                     if ($aSearch['sCountryCode'] === false) {
693                                         $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
694                                         // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
695                                         if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases))) {
696                                             $aSearch['iSearchRank'] += 5;
697                                         }
698                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
699                                     }
700                                 } elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null) {
701                                     if ($aSearch['fLat'] === '') {
702                                         $aSearch['fLat'] = $aSearchTerm['lat'];
703                                         $aSearch['fLon'] = $aSearchTerm['lon'];
704                                         $aSearch['fRadius'] = $aSearchTerm['radius'];
705                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
706                                     }
707                                 } elseif ($sPhraseType == 'postalcode') {
708                                     // We need to try the case where the postal code is the primary element (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode) so try both
709                                     if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
710                                         // If we already have a name try putting the postcode first
711                                         if (sizeof($aSearch['aName'])) {
712                                             $aNewSearch = $aSearch;
713                                             $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
714                                             $aNewSearch['aName'] = array();
715                                             $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
716                                             if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
717                                         }
718
719                                         if (sizeof($aSearch['aName'])) {
720                                             if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false)) {
721                                                 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
722                                             } else {
723                                                 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
724                                                 $aSearch['iSearchRank'] += 1000; // skip;
725                                             }
726                                         } else {
727                                             $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
728                                             //$aSearch['iNamePhrase'] = $iPhrase;
729                                         }
730                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
731                                     }
732                                 } elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house') {
733                                     if ($aSearch['sHouseNumber'] === '') {
734                                         $aSearch['sHouseNumber'] = $sToken;
735                                         // sanity check: if the housenumber is not mainly made
736                                         // up of numbers, add a penalty
737                                         if (preg_match_all("/[^0-9]/", $sToken, $aMatches) > 2) $aSearch['iSearchRank']++;
738                                         // also housenumbers should appear in the first or second phrase
739                                         if ($iPhrase > 1) $aSearch['iSearchRank'] += 1;
740                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
741                                         /*
742                                         // Fall back to not searching for this item (better than nothing)
743                                         $aSearch = $aCurrentSearch;
744                                         $aSearch['iSearchRank'] += 1;
745                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
746                                          */
747                                     }
748                                 } elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null) {
749                                     if ($aSearch['sClass'] === '') {
750                                         $aSearch['sOperator'] = $aSearchTerm['operator'];
751                                         $aSearch['sClass'] = $aSearchTerm['class'];
752                                         $aSearch['sType'] = $aSearchTerm['type'];
753                                         if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
754                                         else $aSearch['sOperator'] = 'near'; // near = in for the moment
755                                         if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
756
757                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
758                                     }
759                                 } elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
760                                     if (sizeof($aSearch['aName'])) {
761                                         if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false)) {
762                                             $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
763                                         } else {
764                                             $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
765                                             $aSearch['iSearchRank'] += 1000; // skip;
766                                         }
767                                     } else {
768                                         $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
769                                         //$aSearch['iNamePhrase'] = $iPhrase;
770                                     }
771                                     if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
772                                 }
773                             }
774                         }
775                         // Look for partial matches.
776                         // Note that there is no point in adding country terms here
777                         // because country are omitted in the address.
778                         if (isset($aValidTokens[$sToken]) && $sPhraseType != 'country') {
779                             // Allow searching for a word - but at extra cost
780                             foreach ($aValidTokens[$sToken] as $aSearchTerm) {
781                                 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
782                                     if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strpos($sToken, ' ') === false) {
783                                         $aSearch = $aCurrentSearch;
784                                         $aSearch['iSearchRank'] += 1;
785                                         if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency) {
786                                             $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
787                                             if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
788                                         } elseif (isset($aValidTokens[' '.$sToken])) { // revert to the token version?
789                                             $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
790                                             $aSearch['iSearchRank'] += 1;
791                                             if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
792                                             foreach ($aValidTokens[' '.$sToken] as $aSearchTermToken) {
793                                                 if (empty($aSearchTermToken['country_code'])
794                                                     && empty($aSearchTermToken['lat'])
795                                                     && empty($aSearchTermToken['class'])
796                                                 ) {
797                                                     $aSearch = $aCurrentSearch;
798                                                     $aSearch['iSearchRank'] += 1;
799                                                     $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
800                                                     if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
801                                                 }
802                                             }
803                                         } else {
804                                             $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
805                                             if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
806                                             if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
807                                         }
808                                     }
809
810                                     if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase) {
811                                         $aSearch = $aCurrentSearch;
812                                         $aSearch['iSearchRank'] += 1;
813                                         if (!sizeof($aCurrentSearch['aName'])) $aSearch['iSearchRank'] += 1;
814                                         if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
815                                         if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency) {
816                                             $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
817                                         } else {
818                                             $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
819                                         }
820                                         $aSearch['iNamePhrase'] = $iPhrase;
821                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
822                                     }
823                                 }
824                             }
825                         } else {
826                             // Allow skipping a word - but at EXTREAM cost
827                             //$aSearch = $aCurrentSearch;
828                             //$aSearch['iSearchRank']+=100;
829                             //$aNewWordsetSearches[] = $aSearch;
830                         }
831                     }
832                     // Sort and cut
833                     usort($aNewWordsetSearches, 'bySearchRank');
834                     $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
835                 }
836                 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
837
838                 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
839                 usort($aNewPhraseSearches, 'bySearchRank');
840
841                 $aSearchHash = array();
842                 foreach ($aNewPhraseSearches as $iSearch => $aSearch) {
843                     $sHash = serialize($aSearch);
844                     if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
845                     else $aSearchHash[$sHash] = 1;
846                 }
847
848                 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
849             }
850
851             // Re-group the searches by their score, junk anything over 20 as just not worth trying
852             $aGroupedSearches = array();
853             foreach ($aNewPhraseSearches as $aSearch) {
854                 if ($aSearch['iSearchRank'] < $this->iMaxRank) {
855                     if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
856                     $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
857                 }
858             }
859             ksort($aGroupedSearches);
860
861             $iSearchCount = 0;
862             $aSearches = array();
863             foreach ($aGroupedSearches as $iScore => $aNewSearches) {
864                 $iSearchCount += sizeof($aNewSearches);
865                 $aSearches = array_merge($aSearches, $aNewSearches);
866                 if ($iSearchCount > 50) break;
867             }
868
869             //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
870         }
871         return $aGroupedSearches;
872     }
873
874     /* Perform the actual query lookup.
875
876         Returns an ordered list of results, each with the following fields:
877             osm_type: type of corresponding OSM object
878                         N - node
879                         W - way
880                         R - relation
881                         P - postcode (internally computed)
882             osm_id: id of corresponding OSM object
883             class: general object class (corresponds to tag key of primary OSM tag)
884             type: subclass of object (corresponds to tag value of primary OSM tag)
885             admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
886             rank_search: rank in search hierarchy
887                         (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
888             rank_address: rank in address hierarchy (determines orer in address)
889             place_id: internal key (may differ between different instances)
890             country_code: ISO country code
891             langaddress: localized full address
892             placename: localized name of object
893             ref: content of ref tag (if available)
894             lon: longitude
895             lat: latitude
896             importance: importance of place based on Wikipedia link count
897             addressimportance: cumulated importance of address elements
898             extra_place: type of place (for admin boundaries, if there is a place tag)
899             aBoundingBox: bounding Box
900             label: short description of the object class/type (English only)
901             name: full name (currently the same as langaddress)
902             foundorder: secondary ordering for places with same importance
903     */
904
905
906     public function lookup()
907     {
908         if (!$this->sQuery && !$this->aStructuredQuery) return false;
909
910         $sLanguagePrefArraySQL = "ARRAY[".join(',', array_map("getDBQuoted", $this->aLangPrefOrder))."]";
911         $sCountryCodesSQL = false;
912         if ($this->aCountryCodes) {
913             $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
914         }
915
916         $sQuery = $this->sQuery;
917         if (!preg_match('//u', $sQuery)) {
918             userError("Query string is not UTF-8 encoded.");
919         }
920
921         // Conflicts between US state abreviations and various words for 'the' in different languages
922         if (isset($this->aLangPrefOrder['name:en'])) {
923             $sQuery = preg_replace('/(^|,)\s*il\s*(,|$)/', '\1illinois\2', $sQuery);
924             $sQuery = preg_replace('/(^|,)\s*al\s*(,|$)/', '\1alabama\2', $sQuery);
925             $sQuery = preg_replace('/(^|,)\s*la\s*(,|$)/', '\1louisiana\2', $sQuery);
926         }
927
928         $bBoundingBoxSearch = $this->bBoundedSearch && $this->sViewboxSmallSQL;
929         if ($this->sViewboxCentreSQL) {
930             // For complex viewboxes (routes) precompute the bounding geometry
931             $sGeom = chksql(
932                 $this->oDB->getOne("select ".$this->sViewboxSmallSQL),
933                 "Could not get small viewbox"
934             );
935             $this->sViewboxSmallSQL = "'".$sGeom."'::geometry";
936
937             $sGeom = chksql(
938                 $this->oDB->getOne("select ".$this->sViewboxLargeSQL),
939                 "Could not get large viewbox"
940             );
941             $this->sViewboxLargeSQL = "'".$sGeom."'::geometry";
942         }
943
944         // Do we have anything that looks like a lat/lon pair?
945         if ($aLooksLike = looksLikeLatLonPair($sQuery)) {
946             $this->setNearPoint(array($aLooksLike['lat'], $aLooksLike['lon']));
947             $sQuery = $aLooksLike['query'];
948         }
949
950         $aSearchResults = array();
951         if ($sQuery || $this->aStructuredQuery) {
952             // Start with a blank search
953             $aSearches = array(
954                           array(
955                            'iSearchRank' => 0,
956                            'iNamePhrase' => -1,
957                            'sCountryCode' => false,
958                            'aName' => array(),
959                            'aAddress' => array(),
960                            'aFullNameAddress' => array(),
961                            'aNameNonSearch' => array(),
962                            'aAddressNonSearch' => array(),
963                            'sOperator' => '',
964                            'aFeatureName' => array(),
965                            'sClass' => '',
966                            'sType' => '',
967                            'sHouseNumber' => '',
968                            'fLat' => '',
969                            'fLon' => '',
970                            'fRadius' => ''
971                           )
972                          );
973
974             // Do we have a radius search?
975             $sNearPointSQL = false;
976             if ($this->aNearPoint) {
977                 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
978                 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
979                 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
980                 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
981             }
982
983             // Any 'special' terms in the search?
984             $bSpecialTerms = false;
985             preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
986             $aSpecialTerms = array();
987             foreach ($aSpecialTermsRaw as $aSpecialTerm) {
988                 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
989                 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
990             }
991
992             preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
993             $aSpecialTerms = array();
994             if (isset($this->aStructuredQuery['amenity']) && $this->aStructuredQuery['amenity']) {
995                 $aSpecialTermsRaw[] = array('['.$this->aStructuredQuery['amenity'].']', $this->aStructuredQuery['amenity']);
996                 unset($this->aStructuredQuery['amenity']);
997             }
998
999             foreach ($aSpecialTermsRaw as $aSpecialTerm) {
1000                 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
1001                 $sToken = chksql($this->oDB->getOne("SELECT make_standard_name('".$aSpecialTerm[1]."') AS string"));
1002                 $sSQL = 'SELECT * ';
1003                 $sSQL .= 'FROM ( ';
1004                 $sSQL .= '   SELECT word_id, word_token, word, class, type, country_code, operator';
1005                 $sSQL .= '   FROM word ';
1006                 $sSQL .= '   WHERE word_token in (\' '.$sToken.'\')';
1007                 $sSQL .= ') AS x ';
1008                 $sSQL .= ' WHERE (class is not null AND class not in (\'place\')) ';
1009                 $sSQL .= ' OR country_code is not null';
1010                 if (CONST_Debug) var_Dump($sSQL);
1011                 $aSearchWords = chksql($this->oDB->getAll($sSQL));
1012                 $aNewSearches = array();
1013                 foreach ($aSearches as $aSearch) {
1014                     foreach ($aSearchWords as $aSearchTerm) {
1015                         $aNewSearch = $aSearch;
1016                         if ($aSearchTerm['country_code']) {
1017                             $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
1018                             $aNewSearches[] = $aNewSearch;
1019                             $bSpecialTerms = true;
1020                         }
1021                         if ($aSearchTerm['class']) {
1022                             $aNewSearch['sClass'] = $aSearchTerm['class'];
1023                             $aNewSearch['sType'] = $aSearchTerm['type'];
1024                             $aNewSearches[] = $aNewSearch;
1025                             $bSpecialTerms = true;
1026                         }
1027                     }
1028                 }
1029                 $aSearches = $aNewSearches;
1030             }
1031
1032             // Split query into phrases
1033             // Commas are used to reduce the search space by indicating where phrases split
1034             if ($this->aStructuredQuery) {
1035                 $aPhrases = $this->aStructuredQuery;
1036                 $bStructuredPhrases = true;
1037             } else {
1038                 $aPhrases = explode(',', $sQuery);
1039                 $bStructuredPhrases = false;
1040             }
1041
1042             // Convert each phrase to standard form
1043             // Create a list of standard words
1044             // Get all 'sets' of words
1045             // Generate a complete list of all
1046             $aTokens = array();
1047             foreach ($aPhrases as $iPhrase => $sPhrase) {
1048                 $aPhrase = chksql(
1049                     $this->oDB->getRow("SELECT make_standard_name('".pg_escape_string($sPhrase)."') as string"),
1050                     "Cannot normalize query string (is it a UTF-8 string?)"
1051                 );
1052                 if (trim($aPhrase['string'])) {
1053                     $aPhrases[$iPhrase] = $aPhrase;
1054                     $aPhrases[$iPhrase]['words'] = explode(' ', $aPhrases[$iPhrase]['string']);
1055                     $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
1056                     $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
1057                 } else {
1058                     unset($aPhrases[$iPhrase]);
1059                 }
1060             }
1061
1062             // Reindex phrases - we make assumptions later on that they are numerically keyed in order
1063             $aPhraseTypes = array_keys($aPhrases);
1064             $aPhrases = array_values($aPhrases);
1065
1066             if (sizeof($aTokens)) {
1067                 // Check which tokens we have, get the ID numbers
1068                 $sSQL = 'SELECT word_id, word_token, word, class, type, country_code, operator, search_name_count';
1069                 $sSQL .= ' FROM word ';
1070                 $sSQL .= ' WHERE word_token in ('.join(',', array_map("getDBQuoted", $aTokens)).')';
1071
1072                 if (CONST_Debug) var_Dump($sSQL);
1073
1074                 $aValidTokens = array();
1075                 if (sizeof($aTokens)) {
1076                     $aDatabaseWords = chksql(
1077                         $this->oDB->getAll($sSQL),
1078                         "Could not get word tokens."
1079                     );
1080                 } else {
1081                     $aDatabaseWords = array();
1082                 }
1083                 $aPossibleMainWordIDs = array();
1084                 $aWordFrequencyScores = array();
1085                 foreach ($aDatabaseWords as $aToken) {
1086                     // Very special case - require 2 letter country param to match the country code found
1087                     if ($bStructuredPhrases && $aToken['country_code'] && !empty($this->aStructuredQuery['country'])
1088                         && strlen($this->aStructuredQuery['country']) == 2 && strtolower($this->aStructuredQuery['country']) != $aToken['country_code']
1089                     ) {
1090                         continue;
1091                     }
1092
1093                     if (isset($aValidTokens[$aToken['word_token']])) {
1094                         $aValidTokens[$aToken['word_token']][] = $aToken;
1095                     } else {
1096                         $aValidTokens[$aToken['word_token']] = array($aToken);
1097                     }
1098                     if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
1099                     $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
1100                 }
1101                 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
1102
1103                 // Try and calculate GB postcodes we might be missing
1104                 foreach ($aTokens as $sToken) {
1105                     // Source of gb postcodes is now definitive - always use
1106                     if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData)) {
1107                         if (substr($aData[1], -2, 1) != ' ') {
1108                             $aData[0] = substr($aData[0], 0, strlen($aData[1])-1).' '.substr($aData[0], strlen($aData[1])-1);
1109                             $aData[1] = substr($aData[1], 0, -1).' '.substr($aData[1], -1, 1);
1110                         }
1111                         $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
1112                         if ($aGBPostcodeLocation) {
1113                             $aValidTokens[$sToken] = $aGBPostcodeLocation;
1114                         }
1115                     } elseif (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData)) {
1116                         // US ZIP+4 codes - if there is no token,
1117                         // merge in the 5-digit ZIP code
1118                         if (isset($aValidTokens[$aData[1]])) {
1119                             foreach ($aValidTokens[$aData[1]] as $aToken) {
1120                                 if (!$aToken['class']) {
1121                                     if (isset($aValidTokens[$sToken])) {
1122                                         $aValidTokens[$sToken][] = $aToken;
1123                                     } else {
1124                                         $aValidTokens[$sToken] = array($aToken);
1125                                     }
1126                                 }
1127                             }
1128                         }
1129                     }
1130                 }
1131
1132                 foreach ($aTokens as $sToken) {
1133                     // Unknown single word token with a number - assume it is a house number
1134                     if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken, ' ') === false && preg_match('/[0-9]/', $sToken)) {
1135                         $aValidTokens[' '.$sToken] = array(array('class' => 'place', 'type' => 'house'));
1136                     }
1137                 }
1138
1139                 // Any words that have failed completely?
1140                 // TODO: suggestions
1141
1142                 // Start the search process
1143                 // array with: placeid => -1 | tiger-housenumber
1144                 $aResultPlaceIDs = array();
1145
1146                 $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases);
1147
1148                 if ($this->bReverseInPlan) {
1149                     // Reverse phrase array and also reverse the order of the wordsets in
1150                     // the first and final phrase. Don't bother about phrases in the middle
1151                     // because order in the address doesn't matter.
1152                     $aPhrases = array_reverse($aPhrases);
1153                     $aPhrases[0]['wordsets'] = getInverseWordSets($aPhrases[0]['words'], 0);
1154                     if (sizeof($aPhrases) > 1) {
1155                         $aFinalPhrase = end($aPhrases);
1156                         $aPhrases[sizeof($aPhrases)-1]['wordsets'] = getInverseWordSets($aFinalPhrase['words'], 0);
1157                     }
1158                     $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, null, $aPhrases, $aValidTokens, $aWordFrequencyScores, false);
1159
1160                     foreach ($aGroupedSearches as $aSearches) {
1161                         foreach ($aSearches as $aSearch) {
1162                             if ($aSearch['iSearchRank'] < $this->iMaxRank) {
1163                                 if (!isset($aReverseGroupedSearches[$aSearch['iSearchRank']])) $aReverseGroupedSearches[$aSearch['iSearchRank']] = array();
1164                                 $aReverseGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1165                             }
1166                         }
1167                     }
1168
1169                     $aGroupedSearches = $aReverseGroupedSearches;
1170                     ksort($aGroupedSearches);
1171                 }
1172             } else {
1173                 // Re-group the searches by their score, junk anything over 20 as just not worth trying
1174                 $aGroupedSearches = array();
1175                 foreach ($aSearches as $aSearch) {
1176                     if ($aSearch['iSearchRank'] < $this->iMaxRank) {
1177                         if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
1178                         $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1179                     }
1180                 }
1181                 ksort($aGroupedSearches);
1182             }
1183
1184             if (CONST_Debug) var_Dump($aGroupedSearches);
1185             if (CONST_Search_TryDroppedAddressTerms && sizeof($this->aStructuredQuery) > 0) {
1186                 $aCopyGroupedSearches = $aGroupedSearches;
1187                 foreach ($aCopyGroupedSearches as $iGroup => $aSearches) {
1188                     foreach ($aSearches as $iSearch => $aSearch) {
1189                         $aReductionsList = array($aSearch['aAddress']);
1190                         $iSearchRank = $aSearch['iSearchRank'];
1191                         while (sizeof($aReductionsList) > 0) {
1192                             $iSearchRank += 5;
1193                             if ($iSearchRank > iMaxRank) break 3;
1194                             $aNewReductionsList = array();
1195                             foreach ($aReductionsList as $aReductionsWordList) {
1196                                 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++) {
1197                                     $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
1198                                     $aReverseSearch = $aSearch;
1199                                     $aSearch['aAddress'] = $aReductionsWordListResult;
1200                                     $aSearch['iSearchRank'] = $iSearchRank;
1201                                     $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
1202                                     if (sizeof($aReductionsWordListResult) > 0) {
1203                                         $aNewReductionsList[] = $aReductionsWordListResult;
1204                                     }
1205                                 }
1206                             }
1207                             $aReductionsList = $aNewReductionsList;
1208                         }
1209                     }
1210                 }
1211                 ksort($aGroupedSearches);
1212             }
1213
1214             // Filter out duplicate searches
1215             $aSearchHash = array();
1216             foreach ($aGroupedSearches as $iGroup => $aSearches) {
1217                 foreach ($aSearches as $iSearch => $aSearch) {
1218                     $sHash = serialize($aSearch);
1219                     if (isset($aSearchHash[$sHash])) {
1220                         unset($aGroupedSearches[$iGroup][$iSearch]);
1221                         if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1222                     } else {
1223                         $aSearchHash[$sHash] = 1;
1224                     }
1225                 }
1226             }
1227
1228             if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1229
1230             $iGroupLoop = 0;
1231             $iQueryLoop = 0;
1232             foreach ($aGroupedSearches as $iGroupedRank => $aSearches) {
1233                 $iGroupLoop++;
1234                 foreach ($aSearches as $aSearch) {
1235                     $iQueryLoop++;
1236                     $searchedHousenumber = -1;
1237
1238                     if (CONST_Debug) echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>";
1239                     if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1240
1241                     // No location term?
1242                     if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon']) {
1243                         if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber']) {
1244                             // Just looking for a country by code - look it up
1245                             if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank) {
1246                                 $sSQL = "SELECT place_id FROM placex WHERE calculated_country_code='".$aSearch['sCountryCode']."' AND rank_search = 4";
1247                                 if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)";
1248                                 if ($bBoundingBoxSearch)
1249                                     $sSQL .= " AND _st_intersects($this->sViewboxSmallSQL, geometry)";
1250                                 $sSQL .= " ORDER BY st_area(geometry) DESC LIMIT 1";
1251                                 if (CONST_Debug) var_dump($sSQL);
1252                                 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1253                             } else {
1254                                 $aPlaceIDs = array();
1255                             }
1256                         } else {
1257                             if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1258                             if (!$aSearch['sClass']) continue;
1259
1260                             $sSQL = "SELECT COUNT(*) FROM pg_tables WHERE tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1261                             if (chksql($this->oDB->getOne($sSQL))) {
1262                                 $sSQL = "SELECT place_id FROM place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1263                                 if ($sCountryCodesSQL) $sSQL .= " JOIN placex USING (place_id)";
1264                                 $sSQL .= " WHERE st_contains($this->sViewboxSmallSQL, ct.centroid)";
1265                                 if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)";
1266                                 if (sizeof($this->aExcludePlaceIDs)) {
1267                                     $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1268                                 }
1269                                 if ($this->sViewboxCentreSQL) $sSQL .= " ORDER BY ST_Distance($this->sViewboxCentreSQL, ct.centroid) ASC";
1270                                 $sSQL .= " limit $this->iLimit";
1271                                 if (CONST_Debug) var_dump($sSQL);
1272                                 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1273
1274                                 // If excluded place IDs are given, it is fair to assume that
1275                                 // there have been results in the small box, so no further
1276                                 // expansion in that case.
1277                                 // Also don't expand if bounded results were requested.
1278                                 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs) && !$this->bBoundedSearch) {
1279                                     $sSQL = "SELECT place_id FROM place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1280                                     if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1281                                     $sSQL .= " WHERE ST_Contains($this->sViewboxLargeSQL, ct.centroid)";
1282                                     if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)";
1283                                     if ($this->sViewboxCentreSQL) $sSQL .= " ORDER BY ST_Distance($this->sViewboxCentreSQL, ct.centroid) ASC";
1284                                     $sSQL .= " LIMIT $this->iLimit";
1285                                     if (CONST_Debug) var_dump($sSQL);
1286                                     $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1287                                 }
1288                             } else {
1289                                 $sSQL = "SELECT place_id ";
1290                                 $sSQL .= "FROM placex ";
1291                                 $sSQL .= "WHERE class='".$aSearch['sClass']."' ";
1292                                 $sSQL .= "  AND type='".$aSearch['sType']."'";
1293                                 $sSQL .= "  AND ST_Contains($this->sViewboxSmallSQL, geometry) ";
1294                                 $sSQL .= "  AND linked_place_id is null";
1295                                 if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)";
1296                                 if ($this->sViewboxCentreSQL)   $sSQL .= " ORDER BY ST_Distance($this->sViewboxCentreSQL, centroid) ASC";
1297                                 $sSQL .= " LIMIT $this->iLimit";
1298                                 if (CONST_Debug) var_dump($sSQL);
1299                                 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1300                             }
1301                         }
1302                     } elseif ($aSearch['fLon'] && !sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['sClass']) {
1303                         // If a coordinate is given, the search must either
1304                         // be for a name or a special search. Ignore everythin else.
1305                         $aPlaceIDs = array();
1306                     } else {
1307                         $aPlaceIDs = array();
1308
1309                         // First we need a position, either aName or fLat or both
1310                         $aTerms = array();
1311                         $aOrder = array();
1312
1313                         if ($aSearch['sHouseNumber'] && sizeof($aSearch['aAddress'])) {
1314                             $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1315                             $aOrder[] = "";
1316                             $aOrder[0] = "  (";
1317                             $aOrder[0] .= "   EXISTS(";
1318                             $aOrder[0] .= "     SELECT place_id ";
1319                             $aOrder[0] .= "     FROM placex ";
1320                             $aOrder[0] .= "     WHERE parent_place_id = search_name.place_id";
1321                             $aOrder[0] .= "       AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."' ";
1322                             $aOrder[0] .= "     LIMIT 1";
1323                             $aOrder[0] .= "   ) ";
1324                             // also housenumbers from interpolation lines table are needed
1325                             $aOrder[0] .= "   OR EXISTS(";
1326                             $aOrder[0] .= "     SELECT place_id ";
1327                             $aOrder[0] .= "     FROM location_property_osmline ";
1328                             $aOrder[0] .= "     WHERE parent_place_id = search_name.place_id";
1329                             $aOrder[0] .= "       AND startnumber is not NULL";
1330                             $aOrder[0] .= "       AND ".intval($aSearch['sHouseNumber']).">=startnumber ";
1331                             $aOrder[0] .= "       AND ".intval($aSearch['sHouseNumber'])."<=endnumber ";
1332                             $aOrder[0] .= "     LIMIT 1";
1333                             $aOrder[0] .= "   )";
1334                             $aOrder[0] .= " )";
1335                             $aOrder[0] .= " DESC";
1336                         }
1337
1338                         // TODO: filter out the pointless search terms (2 letter name tokens and less)
1339                         // they might be right - but they are just too darned expensive to run
1340                         if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'], ",")."]";
1341                         //if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'], ",")."]";
1342                         if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress']) {
1343                             // For infrequent name terms disable index usage for address
1344                             if (CONST_Search_NameOnlySearchFrequencyThreshold
1345                                 && sizeof($aSearch['aName']) == 1
1346                                 && $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold
1347                             ) {
1348                                 //$aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'], $aSearch['aAddressNonSearch']), ",")."]";
1349                                 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddress'],",")."]";
1350                             } else {
1351                                 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'], ",")."]";
1352                                 /*if (sizeof($aSearch['aAddressNonSearch'])) {
1353                                     $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'], ",")."]";
1354                                 }*/
1355                             }
1356                         }
1357                         if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1358                         if ($aSearch['sHouseNumber']) {
1359                             $aTerms[] = "address_rank between 16 and 27";
1360                         } else {
1361                             if ($this->iMinAddressRank > 0) {
1362                                 $aTerms[] = "address_rank >= ".$this->iMinAddressRank;
1363                             }
1364                             if ($this->iMaxAddressRank < 30) {
1365                                 $aTerms[] = "address_rank <= ".$this->iMaxAddressRank;
1366                             }
1367                         }
1368                         if ($aSearch['fLon'] && $aSearch['fLat']) {
1369                             $aTerms[] = sprintf(
1370                                 'ST_DWithin(centroid, ST_SetSRID(ST_Point(%F,%F),4326), %F)',
1371                                 $aSearch['fLon'],
1372                                 $aSearch['fLat'],
1373                                 $aSearch['fRadius']
1374                             );
1375
1376                             $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1377                         }
1378                         if (sizeof($this->aExcludePlaceIDs)) {
1379                             $aTerms[] = "place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1380                         }
1381                         if ($sCountryCodesSQL) {
1382                             $aTerms[] = "country_code in ($sCountryCodesSQL)";
1383                         }
1384
1385                         if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1386                         if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) ASC";
1387
1388                         if ($aSearch['sHouseNumber']) {
1389                             $sImportanceSQL = '- abs(26 - address_rank) + 3';
1390                         } else {
1391                             $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75-(search_rank::float/40) ELSE importance END)';
1392                         }
1393                         if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * CASE WHEN ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1394                         if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * CASE WHEN ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1395
1396                         $aOrder[] = "$sImportanceSQL DESC";
1397                         if (sizeof($aSearch['aFullNameAddress'])) {
1398                             $sExactMatchSQL = ' ( ';
1399                             $sExactMatchSQL .= '   SELECT count(*) FROM ( ';
1400                             $sExactMatchSQL .= '      SELECT unnest(ARRAY['.join($aSearch['aFullNameAddress'], ",").']) ';
1401                             $sExactMatchSQL .= '      INTERSECT ';
1402                             $sExactMatchSQL .= '      SELECT unnest(nameaddress_vector)';
1403                             $sExactMatchSQL .= '   ) s';
1404                             $sExactMatchSQL .= ') as exactmatch';
1405                             $aOrder[] = 'exactmatch DESC';
1406                         } else {
1407                             $sExactMatchSQL = '0::int as exactmatch';
1408                         }
1409
1410                         if (sizeof($aTerms)) {
1411                             $sSQL = "SELECT place_id, ";
1412                             $sSQL .= $sExactMatchSQL;
1413                             $sSQL .= " FROM search_name";
1414                             $sSQL .= " WHERE ".join(' and ', $aTerms);
1415                             $sSQL .= " ORDER BY ".join(', ', $aOrder);
1416                             if ($aSearch['sHouseNumber'] || $aSearch['sClass']) {
1417                                 $sSQL .= " LIMIT 20";
1418                             } elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass']) {
1419                                 $sSQL .= " LIMIT 1";
1420                             } else {
1421                                 $sSQL .= " LIMIT ".$this->iLimit;
1422                             }
1423
1424                             if (CONST_Debug) var_dump($sSQL);
1425                             $aViewBoxPlaceIDs = chksql(
1426                                 $this->oDB->getAll($sSQL),
1427                                 "Could not get places for search terms."
1428                             );
1429                             //var_dump($aViewBoxPlaceIDs);
1430                             // Did we have an viewbox matches?
1431                             $aPlaceIDs = array();
1432                             $bViewBoxMatch = false;
1433                             foreach ($aViewBoxPlaceIDs as $aViewBoxRow) {
1434                                 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1435                                 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1436                                 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1437                                 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1438                                 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1439                                 $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1440                             }
1441                         }
1442                         //var_Dump($aPlaceIDs);
1443                         //exit;
1444
1445                         //now search for housenumber, if housenumber provided
1446                         if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs)) {
1447                             $searchedHousenumber = intval($aSearch['sHouseNumber']);
1448                             $aRoadPlaceIDs = $aPlaceIDs;
1449                             $sPlaceIDs = join(',', $aPlaceIDs);
1450
1451                             // Now they are indexed, look for a house attached to a street we found
1452                             $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1453                             $sSQL = "SELECT place_id FROM placex ";
1454                             $sSQL .= "WHERE parent_place_id in (".$sPlaceIDs.") and transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
1455                             if (sizeof($this->aExcludePlaceIDs)) {
1456                                 $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1457                             }
1458                             $sSQL .= " LIMIT $this->iLimit";
1459                             if (CONST_Debug) var_dump($sSQL);
1460                             $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1461
1462                             // if nothing found, search in the interpolation line table
1463                             if (!sizeof($aPlaceIDs)) {
1464                                 // do we need to use transliteration and the regex for housenumbers???
1465                                 //new query for lines, not housenumbers anymore
1466                                 $sSQL = "SELECT distinct place_id FROM location_property_osmline";
1467                                 $sSQL .= " WHERE startnumber is not NULL and parent_place_id in (".$sPlaceIDs.") and (";
1468                                 if ($searchedHousenumber%2 == 0) {
1469                                     //if housenumber is even, look for housenumber in streets with interpolationtype even or all
1470                                     $sSQL .= "interpolationtype='even'";
1471                                 } else {
1472                                     //look for housenumber in streets with interpolationtype odd or all
1473                                     $sSQL .= "interpolationtype='odd'";
1474                                 }
1475                                 $sSQL .= " or interpolationtype='all') and ";
1476                                 $sSQL .= $searchedHousenumber.">=startnumber and ";
1477                                 $sSQL .= $searchedHousenumber."<=endnumber";
1478
1479                                 if (sizeof($this->aExcludePlaceIDs)) {
1480                                     $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1481                                 }
1482                                 //$sSQL .= " limit $this->iLimit";
1483                                 if (CONST_Debug) var_dump($sSQL);
1484                                 //get place IDs
1485                                 $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0));
1486                             }
1487
1488                             // If nothing found try the aux fallback table
1489                             if (CONST_Use_Aux_Location_data && !sizeof($aPlaceIDs)) {
1490                                 $sSQL = "SELECT place_id FROM location_property_aux ";
1491                                 $sSQL .= " WHERE parent_place_id in (".$sPlaceIDs.") ";
1492                                 $sSQL .= " AND housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1493                                 if (sizeof($this->aExcludePlaceIDs)) {
1494                                     $sSQL .= " AND parent_place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1495                                 }
1496                                 //$sSQL .= " limit $this->iLimit";
1497                                 if (CONST_Debug) var_dump($sSQL);
1498                                 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1499                             }
1500
1501                             //if nothing was found in placex or location_property_aux, then search in Tiger data for this housenumber(location_property_tiger)
1502                             if (CONST_Use_US_Tiger_Data && !sizeof($aPlaceIDs)) {
1503                                 $sSQL = "SELECT distinct place_id FROM location_property_tiger";
1504                                 $sSQL .= " WHERE parent_place_id in (".$sPlaceIDs.") and (";
1505                                 if ($searchedHousenumber%2 == 0) {
1506                                     $sSQL .= "interpolationtype='even'";
1507                                 } else {
1508                                     $sSQL .= "interpolationtype='odd'";
1509                                 }
1510                                 $sSQL .= " or interpolationtype='all') and ";
1511                                 $sSQL .= $searchedHousenumber.">=startnumber and ";
1512                                 $sSQL .= $searchedHousenumber."<=endnumber";
1513
1514                                 if (sizeof($this->aExcludePlaceIDs)) {
1515                                     $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1516                                 }
1517                                 //$sSQL .= " limit $this->iLimit";
1518                                 if (CONST_Debug) var_dump($sSQL);
1519                                 //get place IDs
1520                                 $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0));
1521                             }
1522
1523                             // Fallback to the road (if no housenumber was found)
1524                             if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber'])) {
1525                                 $aPlaceIDs = $aRoadPlaceIDs;
1526                                 //set to -1, if no housenumbers were found
1527                                 $searchedHousenumber = -1;
1528                             }
1529                             //else: housenumber was found, remains saved in searchedHousenumber
1530                         }
1531
1532
1533                         if ($aSearch['sClass'] && sizeof($aPlaceIDs)) {
1534                             $sPlaceIDs = join(',', $aPlaceIDs);
1535                             $aClassPlaceIDs = array();
1536
1537                             if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name') {
1538                                 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1539                                 $sSQL = "SELECT place_id ";
1540                                 $sSQL .= " FROM placex ";
1541                                 $sSQL .= " WHERE place_id in ($sPlaceIDs) ";
1542                                 $sSQL .= "   AND class='".$aSearch['sClass']."' ";
1543                                 $sSQL .= "   AND type='".$aSearch['sType']."'";
1544                                 $sSQL .= "   AND linked_place_id is null";
1545                                 if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)";
1546                                 $sSQL .= " ORDER BY rank_search ASC ";
1547                                 $sSQL .= " LIMIT $this->iLimit";
1548                                 if (CONST_Debug) var_dump($sSQL);
1549                                 $aClassPlaceIDs = chksql($this->oDB->getCol($sSQL));
1550                             }
1551
1552                             if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') { // & in
1553                                 $sSQL = "SELECT count(*) FROM pg_tables ";
1554                                 $sSQL .= "WHERE tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1555                                 $bCacheTable = chksql($this->oDB->getOne($sSQL));
1556
1557                                 $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)";
1558
1559                                 if (CONST_Debug) var_dump($sSQL);
1560                                 $this->iMaxRank = ((int)chksql($this->oDB->getOne($sSQL)));
1561
1562                                 // For state / country level searches the normal radius search doesn't work very well
1563                                 $sPlaceGeom = false;
1564                                 if ($this->iMaxRank < 9 && $bCacheTable) {
1565                                     // Try and get a polygon to search in instead
1566                                     $sSQL = "SELECT geometry ";
1567                                     $sSQL .= " FROM placex";
1568                                     $sSQL .= " WHERE place_id in ($sPlaceIDs)";
1569                                     $sSQL .= "   AND rank_search < $this->iMaxRank + 5";
1570                                     $sSQL .= "   AND ST_Geometrytype(geometry) in ('ST_Polygon','ST_MultiPolygon')";
1571                                     $sSQL .= " ORDER BY rank_search ASC ";
1572                                     $sSQL .= " LIMIT 1";
1573                                     if (CONST_Debug) var_dump($sSQL);
1574                                     $sPlaceGeom = chksql($this->oDB->getOne($sSQL));
1575                                 }
1576
1577                                 if ($sPlaceGeom) {
1578                                     $sPlaceIDs = false;
1579                                 } else {
1580                                     $this->iMaxRank += 5;
1581                                     $sSQL = "SELECT place_id FROM placex WHERE place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1582                                     if (CONST_Debug) var_dump($sSQL);
1583                                     $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1584                                     $sPlaceIDs = join(',', $aPlaceIDs);
1585                                 }
1586
1587                                 if ($sPlaceIDs || $sPlaceGeom) {
1588                                     $fRange = 0.01;
1589                                     if ($bCacheTable) {
1590                                         // More efficient - can make the range bigger
1591                                         $fRange = 0.05;
1592
1593                                         $sOrderBySQL = '';
1594                                         if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1595                                         elseif ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1596                                         elseif ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1597
1598                                         $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1599                                         if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1600                                         if ($sPlaceIDs) {
1601                                             $sSQL .= ",placex as f where ";
1602                                             $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1603                                         }
1604                                         if ($sPlaceGeom) {
1605                                             $sSQL .= " where ";
1606                                             $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1607                                         }
1608                                         if (sizeof($this->aExcludePlaceIDs)) {
1609                                             $sSQL .= " and l.place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1610                                         }
1611                                         if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1612                                         if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1613                                         if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1614                                         $sSQL .= " limit $this->iLimit";
1615                                         if (CONST_Debug) var_dump($sSQL);
1616                                         $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL)));
1617                                     } else {
1618                                         if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1619
1620                                         $sOrderBySQL = '';
1621                                         if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1622                                         else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1623
1624                                         $sSQL = "SELECT distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'');
1625                                         $sSQL .= " FROM placex as l, placex as f ";
1626                                         $sSQL .= " WHERE f.place_id in ($sPlaceIDs) ";
1627                                         $sSQL .= "  AND ST_DWithin(l.geometry, f.centroid, $fRange) ";
1628                                         $sSQL .= "  AND l.class='".$aSearch['sClass']."' ";
1629                                         $sSQL .= "  AND l.type='".$aSearch['sType']."' ";
1630                                         if (sizeof($this->aExcludePlaceIDs)) {
1631                                             $sSQL .= " AND l.place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1632                                         }
1633                                         if ($sCountryCodesSQL) $sSQL .= " AND l.calculated_country_code in ($sCountryCodesSQL)";
1634                                         if ($sOrderBy) $sSQL .= "ORDER BY ".$OrderBysSQL." ASC";
1635                                         if ($this->iOffset) $sSQL .= " OFFSET $this->iOffset";
1636                                         $sSQL .= " limit $this->iLimit";
1637                                         if (CONST_Debug) var_dump($sSQL);
1638                                         $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL)));
1639                                     }
1640                                 }
1641                             }
1642                             $aPlaceIDs = $aClassPlaceIDs;
1643                         }
1644                     }
1645
1646                     if (CONST_Debug) {
1647                         echo "<br><b>Place IDs:</b> ";
1648                         var_Dump($aPlaceIDs);
1649                     }
1650
1651                     foreach ($aPlaceIDs as $iPlaceID) {
1652                         // array for placeID => -1 | Tiger housenumber
1653                         $aResultPlaceIDs[$iPlaceID] = $searchedHousenumber;
1654                     }
1655                     if ($iQueryLoop > 20) break;
1656                 }
1657
1658                 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30)) {
1659                     // Need to verify passes rank limits before dropping out of the loop (yuk!)
1660                     // reduces the number of place ids, like a filter
1661                     // rank_address is 30 for interpolated housenumbers
1662                     $sSQL = "SELECT place_id ";
1663                     $sSQL .= "FROM placex ";
1664                     $sSQL .= "WHERE place_id in (".join(',', array_keys($aResultPlaceIDs)).") ";
1665                     $sSQL .= "  AND (";
1666                     $sSQL .= "         placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1667                     if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) {
1668                         $sSQL .= "     OR (extratags->'place') = 'city'";
1669                     }
1670                     if ($this->aAddressRankList) {
1671                         $sSQL .= "     OR placex.rank_address in (".join(',', $this->aAddressRankList).")";
1672                     }
1673                     if (CONST_Use_US_Tiger_Data) {
1674                         $sSQL .= "  ) ";
1675                         $sSQL .= "UNION ";
1676                         $sSQL .= "  SELECT place_id ";
1677                         $sSQL .= "  FROM location_property_tiger ";
1678                         $sSQL .= "  WHERE place_id in (".join(',', array_keys($aResultPlaceIDs)).") ";
1679                         $sSQL .= "    AND (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1680                         if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',', $this->aAddressRankList).")";
1681                     }
1682                     $sSQL .= ") UNION ";
1683                     $sSQL .= "  SELECT place_id ";
1684                     $sSQL .= "  FROM location_property_osmline ";
1685                     $sSQL .= "  WHERE place_id in (".join(',', array_keys($aResultPlaceIDs)).")";
1686                     $sSQL .= "    AND startnumber is not NULL AND (30 between $this->iMinAddressRank and $this->iMaxAddressRank)";
1687                     if (CONST_Debug) var_dump($sSQL);
1688                     $aFilteredPlaceIDs = chksql($this->oDB->getCol($sSQL));
1689                     $tempIDs = array();
1690                     foreach ($aFilteredPlaceIDs as $placeID) {
1691                         $tempIDs[$placeID] = $aResultPlaceIDs[$placeID];  //assign housenumber to placeID
1692                     }
1693                     $aResultPlaceIDs = $tempIDs;
1694                 }
1695
1696                 //exit;
1697                 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1698                 if ($iGroupLoop > 4) break;
1699                 if ($iQueryLoop > 30) break;
1700             }
1701
1702             // Did we find anything?
1703             if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) {
1704                 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1705             }
1706         } else {
1707             // Just interpret as a reverse geocode
1708             $oReverse = new ReverseGeocode($this->oDB);
1709             $oReverse->setZoom(18);
1710
1711             $aLookup = $oReverse->lookup(
1712                 (float)$this->aNearPoint[0],
1713                 (float)$this->aNearPoint[1],
1714                 false
1715             );
1716
1717             if (CONST_Debug) var_dump("Reverse search", $aLookup);
1718
1719             if ($aLookup['place_id']) {
1720                 $aSearchResults = $this->getDetails(array($aLookup['place_id'] => -1));
1721                 $aResultPlaceIDs[$aLookup['place_id']] = -1;
1722             } else {
1723                 $aSearchResults = array();
1724             }
1725         }
1726
1727         // No results? Done
1728         if (!sizeof($aSearchResults)) {
1729             if ($this->bFallback) {
1730                 if ($this->fallbackStructuredQuery()) {
1731                     return $this->lookup();
1732                 }
1733             }
1734
1735             return array();
1736         }
1737
1738         $aClassType = getClassTypesWithImportance();
1739         $aRecheckWords = preg_split('/\b[\s,\\-]*/u', $sQuery);
1740         foreach ($aRecheckWords as $i => $sWord) {
1741             if (!preg_match('/\pL/', $sWord)) unset($aRecheckWords[$i]);
1742         }
1743
1744         if (CONST_Debug) {
1745             echo '<i>Recheck words:<\i>';
1746             var_dump($aRecheckWords);
1747         }
1748
1749         $oPlaceLookup = new PlaceLookup($this->oDB);
1750         $oPlaceLookup->setIncludePolygonAsPoints($this->bIncludePolygonAsPoints);
1751         $oPlaceLookup->setIncludePolygonAsText($this->bIncludePolygonAsText);
1752         $oPlaceLookup->setIncludePolygonAsGeoJSON($this->bIncludePolygonAsGeoJSON);
1753         $oPlaceLookup->setIncludePolygonAsKML($this->bIncludePolygonAsKML);
1754         $oPlaceLookup->setIncludePolygonAsSVG($this->bIncludePolygonAsSVG);
1755         $oPlaceLookup->setPolygonSimplificationThreshold($this->fPolygonSimplificationThreshold);
1756
1757         foreach ($aSearchResults as $iResNum => $aResult) {
1758             // Default
1759             $fDiameter = getResultDiameter($aResult);
1760
1761             $aOutlineResult = $oPlaceLookup->getOutlines($aResult['place_id'], $aResult['lon'], $aResult['lat'], $fDiameter/2);
1762             if ($aOutlineResult) {
1763                 $aResult = array_merge($aResult, $aOutlineResult);
1764             }
1765             
1766             if ($aResult['extra_place'] == 'city') {
1767                 $aResult['class'] = 'place';
1768                 $aResult['type'] = 'city';
1769                 $aResult['rank_search'] = 16;
1770             }
1771
1772             // Is there an icon set for this type of result?
1773             if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1774                 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon']
1775             ) {
1776                 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1777             }
1778
1779             if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1780                 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label']
1781             ) {
1782                 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1783             } elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1784                 && $aClassType[$aResult['class'].':'.$aResult['type']]['label']
1785             ) {
1786                 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1787             }
1788             // if tag '&addressdetails=1' is set in query
1789             if ($this->bIncludeAddressDetails) {
1790                 // getAddressDetails() is defined in lib.php and uses the SQL function get_addressdata in functions.sql
1791                 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code'], $aResultPlaceIDs[$aResult['place_id']]);
1792                 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city'])) {
1793                     $aResult['address'] = array_merge(array('city' => array_values($aResult['address'])[0]), $aResult['address']);
1794                 }
1795             }
1796
1797             if ($this->bIncludeExtraTags) {
1798                 if ($aResult['extra']) {
1799                     $aResult['sExtraTags'] = json_decode($aResult['extra']);
1800                 } else {
1801                     $aResult['sExtraTags'] = (object) array();
1802                 }
1803             }
1804
1805             if ($this->bIncludeNameDetails) {
1806                 if ($aResult['names']) {
1807                     $aResult['sNameDetails'] = json_decode($aResult['names']);
1808                 } else {
1809                     $aResult['sNameDetails'] = (object) array();
1810                 }
1811             }
1812
1813             // Adjust importance for the number of exact string matches in the result
1814             $aResult['importance'] = max(0.001, $aResult['importance']);
1815             $iCountWords = 0;
1816             $sAddress = $aResult['langaddress'];
1817             foreach ($aRecheckWords as $i => $sWord) {
1818                 if (stripos($sAddress, $sWord)!==false) {
1819                     $iCountWords++;
1820                     if (preg_match("/(^|,)\s*".preg_quote($sWord, '/')."\s*(,|$)/", $sAddress)) $iCountWords += 0.1;
1821                 }
1822             }
1823
1824             $aResult['importance'] = $aResult['importance'] + ($iCountWords*0.1); // 0.1 is a completely arbitrary number but something in the range 0.1 to 0.5 would seem right
1825
1826             $aResult['name'] = $aResult['langaddress'];
1827             // secondary ordering (for results with same importance (the smaller the better):
1828             // - approximate importance of address parts
1829             $aResult['foundorder'] = -$aResult['addressimportance']/10;
1830             // - number of exact matches from the query
1831             if (isset($this->exactMatchCache[$aResult['place_id']])) {
1832                 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1833             } elseif (isset($this->exactMatchCache[$aResult['parent_place_id']])) {
1834                 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1835             }
1836             // - importance of the class/type
1837             if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1838                 && $aClassType[$aResult['class'].':'.$aResult['type']]['importance']
1839             ) {
1840                 $aResult['foundorder'] += 0.0001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1841             } else {
1842                 $aResult['foundorder'] += 0.01;
1843             }
1844             if (CONST_Debug) var_dump($aResult);
1845             $aSearchResults[$iResNum] = $aResult;
1846         }
1847         uasort($aSearchResults, 'byImportance');
1848
1849         $aOSMIDDone = array();
1850         $aClassTypeNameDone = array();
1851         $aToFilter = $aSearchResults;
1852         $aSearchResults = array();
1853
1854         $bFirst = true;
1855         foreach ($aToFilter as $iResNum => $aResult) {
1856             $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1857             if ($bFirst) {
1858                 $fLat = $aResult['lat'];
1859                 $fLon = $aResult['lon'];
1860                 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1861                 $bFirst = false;
1862             }
1863             if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1864                 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']]))
1865             ) {
1866                 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1867                 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1868                 $aSearchResults[] = $aResult;
1869             }
1870
1871             // Absolute limit on number of results
1872             if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1873         }
1874
1875         return $aSearchResults;
1876     } // end lookup()
1877 } // end class