or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

clustering.mdcore-map.mddrawing-shapes.mdindex.mdlayers.mdmarkers-overlays.mdplaces.mdscript-loading.mdservices.md

places.mddocs/

0

# Places Integration

1

2

Components for integrating Google Places API functionality including autocomplete, place search capabilities, and location-based services with comprehensive place data access.

3

4

## Capabilities

5

6

### Autocomplete Component

7

8

Provides place autocomplete functionality for input fields with extensive filtering and customization options.

9

10

```typescript { .api }

11

/**

12

* Provides place autocomplete functionality for input fields

13

* Enhances text inputs with intelligent place suggestions and validation

14

*/

15

interface AutocompleteProps {

16

children: React.ReactNode; // Required - must contain exactly one input element

17

bounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;

18

restrictions?: google.maps.places.ComponentRestrictions;

19

fields?: string[];

20

options?: google.maps.places.AutocompleteOptions;

21

types?: string[];

22

23

// Event handlers

24

onPlaceChanged?: () => void;

25

26

// Lifecycle events

27

onLoad?: (autocomplete: google.maps.places.Autocomplete) => void;

28

onUnmount?: (autocomplete: google.maps.places.Autocomplete) => void;

29

}

30

31

interface google.maps.places.AutocompleteOptions {

32

bounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;

33

componentRestrictions?: google.maps.places.ComponentRestrictions;

34

fields?: string[];

35

placeIdOnly?: boolean;

36

strictBounds?: boolean;

37

types?: string[];

38

}

39

40

interface google.maps.places.ComponentRestrictions {

41

country?: string | string[];

42

}

43

44

function Autocomplete(props: AutocompleteProps): JSX.Element;

45

```

46

47

**Usage Examples:**

48

49

```typescript

50

import React, { useState, useRef } from 'react';

51

import { GoogleMap, LoadScript, Autocomplete } from '@react-google-maps/api';

52

53

// Basic autocomplete

54

function BasicAutocomplete() {

55

const [selectedPlace, setSelectedPlace] = useState<google.maps.places.PlaceResult | null>(null);

56

const autocompleteRef = useRef<google.maps.places.Autocomplete | null>(null);

57

58

const onLoad = (autocomplete: google.maps.places.Autocomplete) => {

59

autocompleteRef.current = autocomplete;

60

};

61

62

const onPlaceChanged = () => {

63

if (autocompleteRef.current) {

64

const place = autocompleteRef.current.getPlace();

65

setSelectedPlace(place);

66

console.log('Selected place:', place);

67

}

68

};

69

70

return (

71

<LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>

72

<div>

73

<div style={{ padding: '10px' }}>

74

<Autocomplete onLoad={onLoad} onPlaceChanged={onPlaceChanged}>

75

<input

76

type="text"

77

placeholder="Enter a place"

78

style={{

79

boxSizing: 'border-box',

80

border: '1px solid transparent',

81

width: '240px',

82

height: '32px',

83

padding: '0 12px',

84

borderRadius: '3px',

85

boxShadow: '0 2px 6px rgba(0, 0, 0, 0.3)',

86

fontSize: '14px',

87

outline: 'none'

88

}}

89

/>

90

</Autocomplete>

91

</div>

92

93

{selectedPlace && (

94

<div style={{ padding: '10px', background: '#f0f0f0' }}>

95

<h4>Selected Place:</h4>

96

<div>Name: {selectedPlace.name}</div>

97

<div>Address: {selectedPlace.formatted_address}</div>

98

{selectedPlace.geometry && (

99

<div>

100

Coordinates: {selectedPlace.geometry.location?.lat()}, {selectedPlace.geometry.location?.lng()}

101

</div>

102

)}

103

</div>

104

)}

105

106

<GoogleMap

107

center={

108

selectedPlace?.geometry?.location

109

? selectedPlace.geometry.location.toJSON()

110

: { lat: 40.7128, lng: -74.0060 }

111

}

112

zoom={15}

113

mapContainerStyle={{ width: '100%', height: '400px' }}

114

/>

115

</div>

116

</LoadScript>

117

);

118

}

119

120

// Advanced autocomplete with restrictions and fields

121

function AdvancedAutocomplete() {

122

const [selectedPlace, setSelectedPlace] = useState<google.maps.places.PlaceResult | null>(null);

123

const [country, setCountry] = useState<string>('us');

124

const [placeType, setPlaceType] = useState<string>('establishment');

125

const autocompleteRef = useRef<google.maps.places.Autocomplete | null>(null);

126

127

// Specify which place data fields to retrieve

128

const placeFields = [

129

'place_id',

130

'name',

131

'formatted_address',

132

'geometry',

133

'types',

134

'rating',

135

'price_level',

136

'opening_hours',

137

'photos',

138

'international_phone_number',

139

'website'

140

];

141

142

const placeTypes = [

143

{ value: 'establishment', label: 'Establishments' },

144

{ value: 'geocode', label: 'Geocodes' },

145

{ value: 'address', label: 'Addresses' },

146

{ value: '(cities)', label: 'Cities' },

147

{ value: '(regions)', label: 'Regions' }

148

];

149

150

const onLoad = (autocomplete: google.maps.places.Autocomplete) => {

151

autocompleteRef.current = autocomplete;

152

};

153

154

const onPlaceChanged = () => {

155

if (autocompleteRef.current) {

156

const place = autocompleteRef.current.getPlace();

157

setSelectedPlace(place);

158

console.log('Place details:', place);

159

}

160

};

161

162

return (

163

<LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>

164

<div>

165

<div style={{ padding: '10px', background: '#f0f0f0' }}>

166

<div style={{ marginBottom: '10px' }}>

167

<label style={{ marginRight: '10px' }}>

168

Country:

169

<select

170

value={country}

171

onChange={(e) => setCountry(e.target.value)}

172

style={{ marginLeft: '5px' }}

173

>

174

<option value="us">United States</option>

175

<option value="ca">Canada</option>

176

<option value="gb">United Kingdom</option>

177

<option value="au">Australia</option>

178

<option value="de">Germany</option>

179

<option value="fr">France</option>

180

</select>

181

</label>

182

183

<label>

184

Place Type:

185

<select

186

value={placeType}

187

onChange={(e) => setPlaceType(e.target.value)}

188

style={{ marginLeft: '5px' }}

189

>

190

{placeTypes.map(type => (

191

<option key={type.value} value={type.value}>

192

{type.label}

193

</option>

194

))}

195

</select>

196

</label>

197

</div>

198

199

<Autocomplete

200

onLoad={onLoad}

201

onPlaceChanged={onPlaceChanged}

202

restrictions={{ country }}

203

types={[placeType]}

204

fields={placeFields}

205

options={{

206

strictBounds: false,

207

componentRestrictions: { country }

208

}}

209

>

210

<input

211

type="text"

212

placeholder={`Search for ${placeTypes.find(t => t.value === placeType)?.label.toLowerCase()} in ${country.toUpperCase()}`}

213

style={{

214

width: '100%',

215

height: '40px',

216

padding: '0 15px',

217

fontSize: '16px',

218

border: '2px solid #4285f4',

219

borderRadius: '8px',

220

outline: 'none',

221

boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)'

222

}}

223

/>

224

</Autocomplete>

225

</div>

226

227

{selectedPlace && (

228

<div style={{ padding: '15px', background: 'white', margin: '10px', borderRadius: '8px', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }}>

229

<h4>{selectedPlace.name}</h4>

230

<div><strong>Address:</strong> {selectedPlace.formatted_address}</div>

231

<div><strong>Types:</strong> {selectedPlace.types?.join(', ')}</div>

232

233

{selectedPlace.rating && (

234

<div><strong>Rating:</strong> {selectedPlace.rating} ⭐</div>

235

)}

236

237

{selectedPlace.price_level !== undefined && (

238

<div><strong>Price Level:</strong> {'$'.repeat(selectedPlace.price_level + 1)}</div>

239

)}

240

241

{selectedPlace.opening_hours && (

242

<div><strong>Open Now:</strong> {selectedPlace.opening_hours.open_now ? 'Yes' : 'No'}</div>

243

)}

244

245

{selectedPlace.international_phone_number && (

246

<div><strong>Phone:</strong> {selectedPlace.international_phone_number}</div>

247

)}

248

249

{selectedPlace.website && (

250

<div>

251

<strong>Website:</strong>

252

<a href={selectedPlace.website} target="_blank" rel="noopener noreferrer" style={{ marginLeft: '5px' }}>

253

{selectedPlace.website}

254

</a>

255

</div>

256

)}

257

258

{selectedPlace.photos && selectedPlace.photos.length > 0 && (

259

<div style={{ marginTop: '10px' }}>

260

<strong>Photos:</strong>

261

<div style={{ display: 'flex', gap: '10px', marginTop: '5px', overflow: 'auto' }}>

262

{selectedPlace.photos.slice(0, 3).map((photo, index) => (

263

<img

264

key={index}

265

src={photo.getUrl({ maxWidth: 200, maxHeight: 150 })}

266

alt={`${selectedPlace.name} photo ${index + 1}`}

267

style={{ borderRadius: '4px', flexShrink: 0 }}

268

/>

269

))}

270

</div>

271

</div>

272

)}

273

</div>

274

)}

275

276

<GoogleMap

277

center={

278

selectedPlace?.geometry?.location

279

? selectedPlace.geometry.location.toJSON()

280

: { lat: 40.7128, lng: -74.0060 }

281

}

282

zoom={selectedPlace ? 16 : 10}

283

mapContainerStyle={{ width: '100%', height: '400px' }}

284

>

285

{selectedPlace && selectedPlace.geometry && (

286

<Marker position={selectedPlace.geometry.location!.toJSON()} />

287

)}

288

</GoogleMap>

289

</div>

290

</LoadScript>

291

);

292

}

293

294

// Autocomplete with bounds restriction

295

function BoundedAutocomplete() {

296

const [mapCenter, setMapCenter] = useState({ lat: 40.7128, lng: -74.0060 });

297

const [mapBounds, setMapBounds] = useState<google.maps.LatLngBounds | null>(null);

298

const [selectedPlace, setSelectedPlace] = useState<google.maps.places.PlaceResult | null>(null);

299

const autocompleteRef = useRef<google.maps.places.Autocomplete | null>(null);

300

const mapRef = useRef<google.maps.Map | null>(null);

301

302

const onMapLoad = (map: google.maps.Map) => {

303

mapRef.current = map;

304

updateBounds(map);

305

};

306

307

const updateBounds = (map: google.maps.Map) => {

308

const bounds = map.getBounds();

309

if (bounds) {

310

setMapBounds(bounds);

311

}

312

};

313

314

const onPlaceChanged = () => {

315

if (autocompleteRef.current) {

316

const place = autocompleteRef.current.getPlace();

317

setSelectedPlace(place);

318

319

if (place.geometry && place.geometry.location) {

320

const location = place.geometry.location.toJSON();

321

setMapCenter(location);

322

323

// Fit map to place if it has a viewport

324

if (place.geometry.viewport && mapRef.current) {

325

mapRef.current.fitBounds(place.geometry.viewport);

326

}

327

}

328

}

329

};

330

331

return (

332

<LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>

333

<div>

334

<div style={{ padding: '10px', background: '#f0f0f0' }}>

335

<div style={{ marginBottom: '10px' }}>

336

<strong>Search within current map area:</strong>

337

</div>

338

339

<Autocomplete

340

onLoad={(autocomplete) => {

341

autocompleteRef.current = autocomplete;

342

}}

343

onPlaceChanged={onPlaceChanged}

344

bounds={mapBounds || undefined}

345

options={{

346

strictBounds: true // Restrict results to the specified bounds

347

}}

348

>

349

<input

350

type="text"

351

placeholder="Search places within map area"

352

style={{

353

width: '100%',

354

height: '40px',

355

padding: '0 15px',

356

fontSize: '16px',

357

border: '1px solid #ccc',

358

borderRadius: '4px'

359

}}

360

/>

361

</Autocomplete>

362

363

{selectedPlace && (

364

<div style={{ marginTop: '10px', padding: '10px', background: 'white', borderRadius: '4px' }}>

365

<strong>{selectedPlace.name}</strong>

366

<div>{selectedPlace.formatted_address}</div>

367

</div>

368

)}

369

</div>

370

371

<GoogleMap

372

center={mapCenter}

373

zoom={13}

374

mapContainerStyle={{ width: '100%', height: '400px' }}

375

onLoad={onMapLoad}

376

onBoundsChanged={() => {

377

if (mapRef.current) {

378

updateBounds(mapRef.current);

379

}

380

}}

381

>

382

{selectedPlace && selectedPlace.geometry && (

383

<Marker position={selectedPlace.geometry.location!.toJSON()} />

384

)}

385

</GoogleMap>

386

</div>

387

</LoadScript>

388

);

389

}

390

```

391

392

### StandaloneSearchBox Component

393

394

Provides place search functionality without the autocomplete dropdown, useful for custom search interfaces.

395

396

```typescript { .api }

397

/**

398

* Provides place search functionality without autocomplete dropdown

399

* Useful for custom search interfaces and batch place searching

400

*/

401

interface StandaloneSearchBoxProps {

402

children: React.ReactNode; // Required - must contain exactly one input element

403

bounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;

404

options?: google.maps.places.SearchBoxOptions;

405

406

// Event handlers

407

onPlacesChanged?: () => void;

408

409

// Lifecycle events

410

onLoad?: (searchBox: google.maps.places.SearchBox) => void;

411

onUnmount?: (searchBox: google.maps.places.SearchBox) => void;

412

}

413

414

interface google.maps.places.SearchBoxOptions {

415

bounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;

416

}

417

418

function StandaloneSearchBox(props: StandaloneSearchBoxProps): JSX.Element;

419

```

420

421

**Usage Examples:**

422

423

```typescript

424

import React, { useState, useRef } from 'react';

425

import { GoogleMap, LoadScript, StandaloneSearchBox, Marker } from '@react-google-maps/api';

426

427

// Basic search box

428

function BasicSearchBox() {

429

const [places, setPlaces] = useState<google.maps.places.PlaceResult[]>([]);

430

const [mapCenter, setMapCenter] = useState({ lat: 40.7128, lng: -74.0060 });

431

const searchBoxRef = useRef<google.maps.places.SearchBox | null>(null);

432

433

const onLoad = (searchBox: google.maps.places.SearchBox) => {

434

searchBoxRef.current = searchBox;

435

};

436

437

const onPlacesChanged = () => {

438

if (searchBoxRef.current) {

439

const places = searchBoxRef.current.getPlaces();

440

441

if (places && places.length > 0) {

442

setPlaces(places);

443

444

// Center map on first result

445

const firstPlace = places[0];

446

if (firstPlace.geometry && firstPlace.geometry.location) {

447

setMapCenter(firstPlace.geometry.location.toJSON());

448

}

449

450

console.log('Search results:', places);

451

}

452

}

453

};

454

455

return (

456

<LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>

457

<div>

458

<div style={{ padding: '10px' }}>

459

<StandaloneSearchBox onLoad={onLoad} onPlacesChanged={onPlacesChanged}>

460

<input

461

type="text"

462

placeholder="Search for places"

463

style={{

464

width: '100%',

465

height: '40px',

466

padding: '0 15px',

467

fontSize: '16px',

468

border: '2px solid #4285f4',

469

borderRadius: '8px',

470

outline: 'none'

471

}}

472

/>

473

</StandaloneSearchBox>

474

</div>

475

476

{places.length > 0 && (

477

<div style={{ padding: '10px', background: '#f0f0f0' }}>

478

<h4>Search Results ({places.length}):</h4>

479

<div style={{ maxHeight: '200px', overflowY: 'auto' }}>

480

{places.map((place, index) => (

481

<div key={index} style={{ padding: '5px', borderBottom: '1px solid #ccc' }}>

482

<strong>{place.name}</strong>

483

<div>{place.formatted_address}</div>

484

</div>

485

))}

486

</div>

487

</div>

488

)}

489

490

<GoogleMap

491

center={mapCenter}

492

zoom={15}

493

mapContainerStyle={{ width: '100%', height: '400px' }}

494

>

495

{places.map((place, index) =>

496

place.geometry && place.geometry.location && (

497

<Marker

498

key={index}

499

position={place.geometry.location.toJSON()}

500

title={place.name}

501

/>

502

)

503

)}

504

</GoogleMap>

505

</div>

506

</LoadScript>

507

);

508

}

509

510

// Advanced search with filtering and categorization

511

function AdvancedSearchBox() {

512

const [places, setPlaces] = useState<google.maps.places.PlaceResult[]>([]);

513

const [filteredPlaces, setFilteredPlaces] = useState<google.maps.places.PlaceResult[]>([]);

514

const [selectedCategory, setSelectedCategory] = useState<string>('all');

515

const [mapBounds, setMapBounds] = useState<google.maps.LatLngBounds | null>(null);

516

const searchBoxRef = useRef<google.maps.places.SearchBox | null>(null);

517

const mapRef = useRef<google.maps.Map | null>(null);

518

519

const categories = [

520

{ value: 'all', label: 'All Places' },

521

{ value: 'restaurant', label: 'Restaurants' },

522

{ value: 'lodging', label: 'Hotels' },

523

{ value: 'tourist_attraction', label: 'Attractions' },

524

{ value: 'store', label: 'Stores' },

525

{ value: 'bank', label: 'Banks' },

526

{ value: 'hospital', label: 'Hospitals' }

527

];

528

529

const onPlacesChanged = () => {

530

if (searchBoxRef.current) {

531

const searchResults = searchBoxRef.current.getPlaces();

532

533

if (searchResults && searchResults.length > 0) {

534

setPlaces(searchResults);

535

filterPlaces(searchResults, selectedCategory);

536

537

// Fit map to show all results

538

if (mapRef.current && searchResults.length > 1) {

539

const bounds = new google.maps.LatLngBounds();

540

searchResults.forEach(place => {

541

if (place.geometry && place.geometry.location) {

542

bounds.extend(place.geometry.location);

543

}

544

});

545

mapRef.current.fitBounds(bounds);

546

}

547

}

548

}

549

};

550

551

const filterPlaces = (placesToFilter: google.maps.places.PlaceResult[], category: string) => {

552

if (category === 'all') {

553

setFilteredPlaces(placesToFilter);

554

} else {

555

const filtered = placesToFilter.filter(place =>

556

place.types?.includes(category as any)

557

);

558

setFilteredPlaces(filtered);

559

}

560

};

561

562

React.useEffect(() => {

563

filterPlaces(places, selectedCategory);

564

}, [selectedCategory, places]);

565

566

const getMarkerColor = (place: google.maps.places.PlaceResult) => {

567

const types = place.types || [];

568

if (types.includes('restaurant' as any)) return 'red';

569

if (types.includes('lodging' as any)) return 'blue';

570

if (types.includes('tourist_attraction' as any)) return 'green';

571

if (types.includes('store' as any)) return 'orange';

572

return 'gray';

573

};

574

575

return (

576

<LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>

577

<div>

578

<div style={{ padding: '10px', background: '#f0f0f0' }}>

579

<div style={{ marginBottom: '10px' }}>

580

<StandaloneSearchBox

581

onLoad={(searchBox) => { searchBoxRef.current = searchBox; }}

582

onPlacesChanged={onPlacesChanged}

583

bounds={mapBounds || undefined}

584

>

585

<input

586

type="text"

587

placeholder="Search for restaurants, hotels, attractions..."

588

style={{

589

width: '100%',

590

height: '40px',

591

padding: '0 15px',

592

fontSize: '16px',

593

border: '1px solid #ccc',

594

borderRadius: '4px'

595

}}

596

/>

597

</StandaloneSearchBox>

598

</div>

599

600

<div style={{ marginBottom: '10px' }}>

601

<label>Filter by category: </label>

602

<select

603

value={selectedCategory}

604

onChange={(e) => setSelectedCategory(e.target.value)}

605

style={{ marginLeft: '10px', padding: '5px' }}

606

>

607

{categories.map(category => (

608

<option key={category.value} value={category.value}>

609

{category.label}

610

</option>

611

))}

612

</select>

613

</div>

614

615

{filteredPlaces.length > 0 && (

616

<div>

617

<div>Showing {filteredPlaces.length} of {places.length} places</div>

618

</div>

619

)}

620

</div>

621

622

{filteredPlaces.length > 0 && (

623

<div style={{

624

height: '150px',

625

overflowY: 'auto',

626

padding: '10px',

627

background: 'white',

628

borderTop: '1px solid #ccc'

629

}}>

630

{filteredPlaces.map((place, index) => (

631

<div

632

key={index}

633

style={{

634

padding: '8px',

635

borderBottom: '1px solid #eee',

636

cursor: 'pointer',

637

display: 'flex',

638

alignItems: 'center'

639

}}

640

onClick={() => {

641

if (place.geometry && place.geometry.location && mapRef.current) {

642

mapRef.current.setCenter(place.geometry.location);

643

mapRef.current.setZoom(16);

644

}

645

}}

646

>

647

<div

648

style={{

649

width: '12px',

650

height: '12px',

651

borderRadius: '50%',

652

backgroundColor: getMarkerColor(place),

653

marginRight: '10px',

654

flexShrink: 0

655

}}

656

/>

657

<div>

658

<div><strong>{place.name}</strong></div>

659

<div style={{ fontSize: '12px', color: '#666' }}>

660

{place.formatted_address}

661

</div>

662

{place.rating && (

663

<div style={{ fontSize: '12px', color: '#666' }}>

664

Rating: {place.rating} ⭐

665

</div>

666

)}

667

</div>

668

</div>

669

))}

670

</div>

671

)}

672

673

<GoogleMap

674

center={{ lat: 40.7128, lng: -74.0060 }}

675

zoom={13}

676

mapContainerStyle={{ width: '100%', height: '400px' }}

677

onLoad={(map) => {

678

mapRef.current = map;

679

}}

680

onBoundsChanged={() => {

681

if (mapRef.current) {

682

const bounds = mapRef.current.getBounds();

683

if (bounds) {

684

setMapBounds(bounds);

685

}

686

}

687

}}

688

>

689

{filteredPlaces.map((place, index) =>

690

place.geometry && place.geometry.location && (

691

<Marker

692

key={index}

693

position={place.geometry.location.toJSON()}

694

title={place.name}

695

icon={`https://maps.google.com/mapfiles/ms/icons/${getMarkerColor(place)}-dot.png`}

696

/>

697

)

698

)}

699

</GoogleMap>

700

</div>

701

</LoadScript>

702

);

703

}

704

705

// Search box with custom UI and place details

706

function CustomSearchInterface() {

707

const [places, setPlaces] = useState<google.maps.places.PlaceResult[]>([]);

708

const [selectedPlace, setSelectedPlace] = useState<google.maps.places.PlaceResult | null>(null);

709

const [searchValue, setSearchValue] = useState('');

710

const searchBoxRef = useRef<google.maps.places.SearchBox | null>(null);

711

712

const performSearch = () => {

713

if (searchBoxRef.current && searchValue.trim()) {

714

// Trigger search by setting the input value and firing the event

715

const input = document.getElementById('search-input') as HTMLInputElement;

716

if (input) {

717

input.value = searchValue;

718

google.maps.event.trigger(input, 'focus');

719

google.maps.event.trigger(input, 'keydown', { keyCode: 13 });

720

}

721

}

722

};

723

724

const onPlacesChanged = () => {

725

if (searchBoxRef.current) {

726

const results = searchBoxRef.current.getPlaces();

727

if (results && results.length > 0) {

728

setPlaces(results);

729

setSelectedPlace(results[0]); // Select first result by default

730

}

731

}

732

};

733

734

return (

735

<LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>

736

<div style={{ display: 'flex', height: '500px' }}>

737

{/* Search Panel */}

738

<div style={{ width: '350px', padding: '15px', background: '#f8f9fa', borderRight: '1px solid #dee2e6' }}>

739

<h3 style={{ margin: '0 0 15px 0' }}>Place Search</h3>

740

741

<div style={{ marginBottom: '15px' }}>

742

<StandaloneSearchBox

743

onLoad={(searchBox) => { searchBoxRef.current = searchBox; }}

744

onPlacesChanged={onPlacesChanged}

745

>

746

<input

747

id="search-input"

748

type="text"

749

value={searchValue}

750

onChange={(e) => setSearchValue(e.target.value)}

751

onKeyPress={(e) => e.key === 'Enter' && performSearch()}

752

placeholder="Search places..."

753

style={{

754

width: '100%',

755

height: '40px',

756

padding: '0 15px',

757

border: '1px solid #ced4da',

758

borderRadius: '4px',

759

fontSize: '14px'

760

}}

761

/>

762

</StandaloneSearchBox>

763

764

<button

765

onClick={performSearch}

766

style={{

767

width: '100%',

768

marginTop: '10px',

769

padding: '10px',

770

background: '#007bff',

771

color: 'white',

772

border: 'none',

773

borderRadius: '4px',

774

cursor: 'pointer'

775

}}

776

>

777

Search

778

</button>

779

</div>

780

781

{/* Results List */}

782

<div style={{ maxHeight: '300px', overflowY: 'auto' }}>

783

{places.map((place, index) => (

784

<div

785

key={index}

786

onClick={() => setSelectedPlace(place)}

787

style={{

788

padding: '10px',

789

margin: '5px 0',

790

background: selectedPlace === place ? '#e3f2fd' : 'white',

791

border: '1px solid #dee2e6',

792

borderRadius: '4px',

793

cursor: 'pointer'

794

}}

795

>

796

<div style={{ fontWeight: 'bold', marginBottom: '5px' }}>

797

{place.name}

798

</div>

799

<div style={{ fontSize: '12px', color: '#666' }}>

800

{place.formatted_address}

801

</div>

802

{place.rating && (

803

<div style={{ fontSize: '12px', marginTop: '5px' }}>

804

⭐ {place.rating} {place.user_ratings_total && `(${place.user_ratings_total} reviews)`}

805

</div>

806

)}

807

</div>

808

))}

809

</div>

810

811

{/* Selected Place Details */}

812

{selectedPlace && (

813

<div style={{

814

marginTop: '15px',

815

padding: '15px',

816

background: 'white',

817

border: '1px solid #dee2e6',

818

borderRadius: '4px'

819

}}>

820

<h4 style={{ margin: '0 0 10px 0' }}>Place Details</h4>

821

<div><strong>Name:</strong> {selectedPlace.name}</div>

822

<div><strong>Address:</strong> {selectedPlace.formatted_address}</div>

823

<div><strong>Types:</strong> {selectedPlace.types?.join(', ')}</div>

824

{selectedPlace.rating && (

825

<div><strong>Rating:</strong> {selectedPlace.rating} / 5</div>

826

)}

827

{selectedPlace.price_level !== undefined && (

828

<div><strong>Price:</strong> {'$'.repeat(selectedPlace.price_level + 1)}</div>

829

)}

830

</div>

831

)}

832

</div>

833

834

{/* Map */}

835

<div style={{ flex: 1 }}>

836

<GoogleMap

837

center={

838

selectedPlace?.geometry?.location

839

? selectedPlace.geometry.location.toJSON()

840

: { lat: 40.7128, lng: -74.0060 }

841

}

842

zoom={selectedPlace ? 16 : 12}

843

mapContainerStyle={{ width: '100%', height: '100%' }}

844

>

845

{places.map((place, index) =>

846

place.geometry && place.geometry.location && (

847

<Marker

848

key={index}

849

position={place.geometry.location.toJSON()}

850

title={place.name}

851

icon={{

852

url: selectedPlace === place

853

? 'https://maps.google.com/mapfiles/ms/icons/red-dot.png'

854

: 'https://maps.google.com/mapfiles/ms/icons/blue-dot.png',

855

scaledSize: new google.maps.Size(32, 32)

856

}}

857

onClick={() => setSelectedPlace(place)}

858

/>

859

)

860

)}

861

</GoogleMap>

862

</div>

863

</div>

864

</LoadScript>

865

);

866

}

867

```

868

869

### Places API Integration Best Practices

870

871

Guidelines for effective Places API usage and optimization strategies.

872

873

```typescript { .api }

874

/**

875

* Places API integration best practices and optimization

876

*/

877

interface PlacesAPIBestPractices {

878

// Field selection optimization

879

requestOnlyNeededFields: boolean; // Reduce API costs by requesting specific fields

880

essentialFields: string[]; // Minimal required fields for basic functionality

881

extendedFields: string[]; // Additional fields for enhanced features

882

883

// Geographic optimization

884

useBounds: boolean; // Restrict searches to relevant geographic areas

885

useStrictBounds: boolean; // Enforce strict boundary compliance

886

887

// Performance optimization

888

implementDebouncing: boolean; // Debounce user input to reduce API calls

889

cacheResults: boolean; // Cache place results for repeated queries

890

useSessionTokens: boolean; // Group related requests for cost optimization

891

}

892

893

// Example field optimization

894

const PLACE_FIELDS = {

895

basic: ['place_id', 'name', 'formatted_address', 'geometry'],

896

contact: ['international_phone_number', 'website', 'opening_hours'],

897

atmosphere: ['rating', 'user_ratings_total', 'price_level', 'reviews'],

898

media: ['photos', 'icon', 'icon_background_color'],

899

detailed: ['types', 'vicinity', 'plus_code', 'url']

900

};

901

902

// Example debounced autocomplete implementation

903

const useDebouncedAutocomplete = (delay: number = 300) => {

904

const [searchTerm, setSearchTerm] = React.useState('');

905

const [debouncedTerm, setDebouncedTerm] = React.useState('');

906

907

React.useEffect(() => {

908

const timer = setTimeout(() => {

909

setDebouncedTerm(searchTerm);

910

}, delay);

911

912

return () => clearTimeout(timer);

913

}, [searchTerm, delay]);

914

915

return { searchTerm, setSearchTerm, debouncedTerm };

916

};

917

```