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