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