RadiusMapper
All articles
job boardcommute mapsmapbox

Job Search Commute Map with Mapbox and Radius Mapper API

Step-by-step guide to adding commute time visualization to your job board platform. Complete with React and Mapbox integration examples.

February 14, 2025|4 min read
Job Search Commute Map with Mapbox and Radius Mapper API

Building a Job Search Commute Map

Help job seekers find their perfect role by commute time. In this guide, I'll show you how to integrate commute time visualization into your job board, allowing users to filter jobs based on their preferred travel time from home.

What We'll Build

We'll create a commute-aware job search that lets users:

  • Enter their home location
  • Set their maximum acceptable commute time
  • View jobs within their preferred commute radius
  • Filter job listings based on commute time
  • Toggle between different transportation modes (driving, walking, cycling)

Quick Start

First, get your API keys:

bash
npm install mapbox-gl @mapbox/mapbox-gl-geocoder

Implementation

Here's a complete React component for job search with commute time filtering:

typescript
import React, { useState, useRef, useEffect } from 'react'; import mapboxgl from 'mapbox-gl'; import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'; import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'; import 'mapbox-gl/dist/mapbox-gl.css'; interface Job { id: string; title: string; company: string; coordinates: [number, number]; // [longitude, latitude] salary: string; } interface CommuteJobMapProps { jobs: Job[]; onJobsFilter: (jobs: Job[]) => void; } const CommuteJobMap: React.FC<CommuteJobMapProps> = ({ jobs, onJobsFilter }) => { const mapContainer = useRef<HTMLDivElement>(null); const map = useRef<mapboxgl.Map | null>(null); const [loading, setLoading] = useState(false); const [settings, setSettings] = useState({ homeAddress: '', maxCommuteTime: 30, transportMode: 'driving' as const }); // Initialize map useEffect(() => { if (!mapContainer.current) return; mapboxgl.accessToken = process.env.MAPBOX_TOKEN; map.current = new mapboxgl.Map({ container: mapContainer.current, style: 'mapbox://styles/mapbox/light-v11', center: [-122.4194, 37.7749], zoom: 11 }); // Add geocoder (address search) const geocoder = new MapboxGeocoder({ accessToken: mapboxgl.accessToken, mapboxgl: mapboxgl, placeholder: 'Enter your home address' }); map.current.addControl(geocoder); // Listen for address selection geocoder.on('result', (e) => { setSettings(prev => ({ ...prev, homeAddress: e.result.place_name })); }); return () => { map.current?.remove(); }; }, []); // Add job markers useEffect(() => { if (!map.current) return; jobs.forEach(job => { const marker = new mapboxgl.Marker() .setLngLat(job.coordinates) .setPopup( new mapboxgl.Popup({ offset: 25 }).setHTML(` <h3 class="font-semibold">${job.title}</h3> <p>${job.company}</p> <p class="text-sm">${job.salary}</p> `) ) .addTo(map.current!); }); }, [jobs]); const fetchCommuteArea = async () => { try { setLoading(true); const response = await fetch('https://radiusmapper.com/api/search', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.RADIUS_MAPPER_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ address: settings.homeAddress, travelTimeMinutes: settings.maxCommuteTime, transportMode: settings.transportMode, outputFormat: 'geojson' }) }); const data = await response.json(); // Add/update the commute area on the map if (map.current?.getSource('commute-area')) { (map.current.getSource('commute-area') as mapboxgl.GeoJSONSource) .setData(data.boundaryGeojson); } else { map.current?.addSource('commute-area', { type: 'geojson', data: data.boundaryGeojson }); map.current?.addLayer({ id: 'commute-area', type: 'fill', source: 'commute-area', paint: { 'fill-color': '#3bb2d0', 'fill-opacity': 0.3, 'fill-outline-color': '#3bb2d0' } }); } // Filter jobs within commute area const jobsWithinCommute = jobs.filter(job => isPointInPolygon( job.coordinates, data.boundaryGeojson.features[0].geometry.coordinates[0] ) ); onJobsFilter(jobsWithinCommute); } catch (error) { console.error('Error fetching commute area:', error); } finally { setLoading(false); } }; return ( <div className="relative"> {/* Commute Settings Panel */} <div className="absolute top-4 right-4 z-10 bg-white p-4 rounded-lg shadow-lg w-80"> <h3 className="text-lg font-semibold mb-4">Commute Preferences</h3> <div className="space-y-4"> <div> <label className="block text-sm font-medium mb-1"> Max Commute Time: {settings.maxCommuteTime} minutes </label> <input type="range" min="5" max="60" value={settings.maxCommuteTime} onChange={(e) => setSettings(prev => ({ ...prev, maxCommuteTime: parseInt(e.target.value) }))} className="w-full" /> </div> <div> <label className="block text-sm font-medium mb-1"> Transport Mode </label> <select value={settings.transportMode} onChange={(e) => setSettings(prev => ({ ...prev, transportMode: e.target.value as typeof settings.transportMode }))} className="w-full p-2 border rounded" > <option value="driving">Driving</option> <option value="transit">Public Transit</option> <option value="cycling">Cycling</option> <option value="walking">Walking</option> </select> </div> <button onClick={fetchCommuteArea} disabled={loading || !settings.homeAddress} className="w-full bg-blue-500 text-white p-2 rounded disabled:opacity-50" > {loading ? 'Calculating...' : 'Update Commute Area'} </button> </div> </div> {/* Map Container */} <div ref={mapContainer} className="w-full h-[600px]" /> </div> ); }; // Usage in your job board app const JobBoard = () => { const [jobs, setJobs] = useState<Job[]>([]); const [filteredJobs, setFilteredJobs] = useState<Job[]>([]); return ( <div className="container mx-auto p-4"> <div className="grid lg:grid-cols-[1fr,400px] gap-8"> <CommuteJobMap jobs={jobs} onJobsFilter={setFilteredJobs} /> <div className="space-y-4"> <h2 className="text-xl font-semibold"> {filteredJobs.length} Jobs Within Commute Range </h2> {filteredJobs.map(job => ( <JobCard key={job.id} job={job} /> ))} </div> </div> </div> ); };

Job Filtering Optimization

For larger job datasets, optimize the filtering:

typescript
// Create a spatial index for faster job filtering import KDBush from 'kdbush'; const jobIndex = new KDBush(jobs.map(job => ({ ...job, coordinates: job.coordinates }))); const getJobsInBounds = (bounds: mapboxgl.LngLatBounds) => { return jobIndex.range( bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth() ).map(id => jobs[id]); };

Commute-Aware Search Features

Add these enhancements to improve the job search experience:

  1. Salary with Commute Cost Calculator:
typescript
const calculateCommutePreferences = (job: Job, commuteDetails: CommuteDetails) => { const yearlyCommuteCost = estimateCommuteCost(commuteDetails); const effectiveSalary = job.salary - yearlyCommuteCost; return { ...job, commuteDetails, effectiveSalary }; };
  1. Smart Job Sorting:
typescript
const sortByCommutePreference = (jobs: Job[]) => { return jobs.sort((a, b) => { // Prioritize jobs with better commute/salary ratio const aScore = a.salary / a.commuteDetails.timeInMinutes; const bScore = b.salary / b.commuteDetails.timeInMinutes; return bScore - aScore; }); };

Performance Tips

  1. Cluster Job Markers:
typescript
map.current.addSource('jobs', { type: 'geojson', data: { type: 'FeatureCollection', features: jobs.map(job => ({ type: 'Feature', geometry: { type: 'Point', coordinates: job.coordinates }, properties: { id: job.id, title: job.title } })) }, cluster: true, clusterMaxZoom: 14, clusterRadius: 50 });
  1. Cache Commute Areas:
typescript
const commuteCache = new Map<string, GeoJSON.FeatureCollection>(); const getCachedCommuteArea = async (settings: CommuteSettings) => { const key = JSON.stringify(settings); if (commuteCache.has(key)) { return commuteCache.get(key); } const result = await fetchCommuteArea(settings); commuteCache.set(key, result); return result; };

Enhanced User Experience

Consider adding these features:

  • Save commute preferences in user profiles
  • Show commute time estimates on job cards
  • Add transit station overlays
  • Include traffic pattern information
  • Allow multiple commute locations (e.g., partner's workplace)

If you prefer Google Maps over Mapbox, see our companion guide on building commute maps with Google Maps. For a no-code alternative, you can use RadiusMapper embeds to add interactive maps without any API integration.

What's Next?

Enhance your job board's commute features with:

  • Real-time traffic updates
  • Hybrid work schedule support
  • Parking availability data
  • Public transit schedules
  • Cycling route information

Check out our API docs for implementation details!

Building a commute-aware job board? Let me know if you have questions! 💼