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