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