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