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