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