How to Build a Recruiting Market Map From a Job Description
A recruiting market map sizes where the talent sits before you pull a single name. Here is how to build one from a job description with a people and company API.
Published
Jun 19, 2026
Written by
Chris Pisarski
Reviewed by
Abhilash Chowdhary
Read time
7
minutes

How to Build a Recruiting Market Map From a Job Description
A founder tells a recruiter there are thousands of people who fit the role. The recruiter we worked with has a move for that moment. He takes the same profile, adds the real filters one by one in front of the client, and the number on screen falls from 11,000 people to 68, with only 12 of them in the Bay Area for a job that wants six days a week in the office. By his own account, that is a real story, and it happens all the time. The point of a recruiting market map is to know those numbers before you pull a single name.
This guide builds that map from a job description. We will go from a raw description to a sized, segmented picture of where the matching talent actually sits, which companies employ it, at what titles, in which places, and how many of each. It is the supply-side view you want before you start sourcing, and it runs on a people and company API rather than a manual sweep of profiles. You can build a first version on a free Crustdata account with 100 credits.
What's a recruiting market map?
A recruiting market map is a count of the talent that matches a role, broken down by where it works, what it is called, and where it lives. It answers a different question from a candidate list. A list names the people you might call. A market map tells you how many of them exist, which companies they cluster in, and how the population thins out as the requirements get stricter.
The team we spoke with described it as a high-level read on the world their client is hiring into. One of their recruiters put it plainly, that a broad map shows a client the world they are actually living in, before anyone argues about a single candidate. That framing is the job. The map sizes the supply, sets expectations, and points your later sourcing at the part of the market worth working.
It also does quiet political work. A market map is what lets a recruiter walk a difficult hiring manager off an unrealistic spec, because the count does the arguing. When the requirement says senior, on-site, six days a week, in one metro, and the map says 12 people, the conversation changes on its own.
Read the job description down to filters
A job description is not yet a market map. It is a paragraph of prose, and most of it is aspirational. Before you can size anything, you have to turn the description into the handful of filters that actually define the population.
That means three passes over the text:
Pull the real disqualifiers: the requirements a candidate genuinely has to meet, separated from the wish-list items written to impress. The seniority floor, the must-have domain, the location and on-site rule are usually disqualifiers. The long tail of nice-to-haves usually is not.
Expand the title into the ones people really use: a robotics role reaches the hardware, embedded, and firmware engineers doing the same work under different labels, so your count includes them instead of missing them on a vocabulary mismatch.
Name the kind of company the talent comes from: the industry, the size band, and the funding stage where this profile tends to sit, which becomes the company side of the map.
Those three things, the criteria, the title variants, and the company profile, are the inputs to every count that follows. The translation step matters because the map inherits any error in it. We go deeper on turning a description into real search criteria in our guide to turning a job description into a sourced candidate list. For the map, the short version is that you are converting prose into filters a query can run.
Find and segment the target companies
The company side of the map answers where this talent works. You start by building the set of companies that employ the profile, then segment that set the way a client thinks about it, by size, stage, and location.
Company Search does this directly. You filter the company universe down to the industry, headcount band, and geography that matches the role, and you get back a segmentable list of target companies. The team we spoke with wanted exactly this kind of cut, the ability to slice a market by stage of company, size of company, and money raised, so they could tell a client where a profile sits and what that profile costs at a Series A company in San Francisco.
A Claude Code agent with Crustdata's MCP server configured can run this for you from a plain-language brief, which is the fastest path if you do not want to write code. There is a recruiting market mapping skill built for this, where you describe the role and it assembles the company and talent breakdown as a report. For teams who want to own the logic, the same data is one direct call.
import requests # Find and segment the target companies that employ this profile: # US software companies, mid-size, with a real engineering org. resp = requests.post( "https://api.crustdata.com/company/search", headers={ "Authorization": "Bearer YOUR_API_KEY", "x-api-version": "2025-11-01", }, json={ "filters": { "op": "and", "conditions": [ {"field": "taxonomy.professional_network_industry", "type": "=", "value": "Software Development"}, {"field": "locations.country", "type": "in", "value": ["USA"]}, {"field": "headcount.total", "type": ">", "value": 200}, {"field": "headcount.total", "type": "<", "value": 2000}, {"field": "roles.distribution.engineering", "type": ">", "value": 50}, ], }, "fields": ["basic_info.name", "basic_info.primary_domain", "headcount.total", "locations.country"], "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 100, }, ) companies = resp.json()["companies"] # Each company is a cell on the map. Segment these by headcount band, # funding stage (funding.last_round_type), or HQ country as the client needs
import requests # Find and segment the target companies that employ this profile: # US software companies, mid-size, with a real engineering org. resp = requests.post( "https://api.crustdata.com/company/search", headers={ "Authorization": "Bearer YOUR_API_KEY", "x-api-version": "2025-11-01", }, json={ "filters": { "op": "and", "conditions": [ {"field": "taxonomy.professional_network_industry", "type": "=", "value": "Software Development"}, {"field": "locations.country", "type": "in", "value": ["USA"]}, {"field": "headcount.total", "type": ">", "value": 200}, {"field": "headcount.total", "type": "<", "value": 2000}, {"field": "roles.distribution.engineering", "type": ">", "value": 50}, ], }, "fields": ["basic_info.name", "basic_info.primary_domain", "headcount.total", "locations.country"], "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 100, }, ) companies = resp.json()["companies"] # Each company is a cell on the map. Segment these by headcount band, # funding stage (funding.last_round_type), or HQ country as the client needs
import requests # Find and segment the target companies that employ this profile: # US software companies, mid-size, with a real engineering org. resp = requests.post( "https://api.crustdata.com/company/search", headers={ "Authorization": "Bearer YOUR_API_KEY", "x-api-version": "2025-11-01", }, json={ "filters": { "op": "and", "conditions": [ {"field": "taxonomy.professional_network_industry", "type": "=", "value": "Software Development"}, {"field": "locations.country", "type": "in", "value": ["USA"]}, {"field": "headcount.total", "type": ">", "value": 200}, {"field": "headcount.total", "type": "<", "value": 2000}, {"field": "roles.distribution.engineering", "type": ">", "value": 50}, ], }, "fields": ["basic_info.name", "basic_info.primary_domain", "headcount.total", "locations.country"], "sorts": [{"field": "headcount.total", "order": "desc"}], "limit": 100, }, ) companies = resp.json()["companies"] # Each company is a cell on the map. Segment these by headcount band, # funding stage (funding.last_round_type), or HQ country as the client needs
The roles.distribution.engineering filter is how you avoid counting companies that carry the right industry tag but employ none of the function you actually want. It is filter-only, so you narrow on it rather than ask for it back. Segment the returned set by headcount.total, funding.last_round_type, or locations.country, and the company layer of the map is done.
Size the population, then watch it shrink
This is the part the founder story is really about. The single most useful number on a market map is the total count of people who match the current filters, because that number is what falls as you add requirements.
People Search returns it for free on every query. You send the filters and read total_count, the number of people in the database who match. You do not have to pull any profiles to get it, so sizing a population costs almost nothing.
import requests def count_matches(filters): resp = requests.post( "https://api.crustdata.com/person/search", headers={ "Authorization": "Bearer YOUR_API_KEY", "x-api-version": "2025-11-01", }, # limit 1 keeps it cheap; total_count is the whole point. json={"filters": filters, "limit": 1}, ) return resp.json()["total_count"] # Start broad: everyone with the core title. broad = count_matches({ "field": "experience.employment_details.current.title", "type": "(.)", "value": "hardware engineer|embedded engineer|firmware engineer", }) # Add the location filter. located = count_matches({ "op": "and", "conditions": [ {"field": "experience.employment_details.current.title", "type": "(.)", "value": "hardware engineer|embedded engineer|firmware engineer"}, {"field": "basic_profile.location.state", "type": "=", "value": "California"}, ], }) print(broad, located) # the population, then the same population after one real filter
import requests def count_matches(filters): resp = requests.post( "https://api.crustdata.com/person/search", headers={ "Authorization": "Bearer YOUR_API_KEY", "x-api-version": "2025-11-01", }, # limit 1 keeps it cheap; total_count is the whole point. json={"filters": filters, "limit": 1}, ) return resp.json()["total_count"] # Start broad: everyone with the core title. broad = count_matches({ "field": "experience.employment_details.current.title", "type": "(.)", "value": "hardware engineer|embedded engineer|firmware engineer", }) # Add the location filter. located = count_matches({ "op": "and", "conditions": [ {"field": "experience.employment_details.current.title", "type": "(.)", "value": "hardware engineer|embedded engineer|firmware engineer"}, {"field": "basic_profile.location.state", "type": "=", "value": "California"}, ], }) print(broad, located) # the population, then the same population after one real filter
import requests def count_matches(filters): resp = requests.post( "https://api.crustdata.com/person/search", headers={ "Authorization": "Bearer YOUR_API_KEY", "x-api-version": "2025-11-01", }, # limit 1 keeps it cheap; total_count is the whole point. json={"filters": filters, "limit": 1}, ) return resp.json()["total_count"] # Start broad: everyone with the core title. broad = count_matches({ "field": "experience.employment_details.current.title", "type": "(.)", "value": "hardware engineer|embedded engineer|firmware engineer", }) # Add the location filter. located = count_matches({ "op": "and", "conditions": [ {"field": "experience.employment_details.current.title", "type": "(.)", "value": "hardware engineer|embedded engineer|firmware engineer"}, {"field": "basic_profile.location.state", "type": "=", "value": "California"}, ], }) print(broad, located) # the population, then the same population after one real filter
Run that progression and you have reproduced the founder move in code. The broad title returns a large number. Add the state, and it drops. Add the seniority floor, the on-site metro, and the must-have domain, and it keeps dropping. The recruiter narrates each step to the client, and the falling count carries the argument. The (.) operator does a regex match, so the pipe in the title value means the search reaches every label for the same job rather than one literal string.
What you are building here is the spine of the map. Each filter you add is a question the client implicitly asked of the role, and each new total_count is the honest answer.
Counting first is also what keeps the spend predictable, which is the other thing the team we spoke with worried about. They were building a system with a lot of it tied to Claude, and they wanted to know the ballpark before the numbers ran away, naming the fear as the month where "we have to pay like $6,000" and think "what just happened." A market map answers that worry head on. The counts are free, so the exploring and re-slicing costs nothing, and the priced step, pulling and enriching real profiles, only ever runs on a population you have already sized. You see the size of a pull before you commit to it, so the bill stays a number you set rather than one that surprises you.
Distribute the talent across companies, titles, and geos
A single number sizes the market. The map gets useful when you break that number into its parts, so you can see the shape of the population rather than just its size.
Three breakdowns carry most of the value, and each is the same count query run across a set of values.
By company
Run the count once per target company from the company list you built, filtering on the current employer. The result is a ranked picture of where the talent concentrates, which is where your sourcing should start.
# For each company on the target list, count current employees who match the profile. for company in companies: n = count_matches({ "op": "and", "conditions": [ {"field": "experience.employment_details.current.company_name", "type": "in", "value": [company["basic_info"]["name"]]}, {"field": "experience.employment_details.current.title", "type": "(.)", "value": "hardware engineer|embedded engineer|firmware engineer"}, ], }) company["matching_talent"] = n
# For each company on the target list, count current employees who match the profile. for company in companies: n = count_matches({ "op": "and", "conditions": [ {"field": "experience.employment_details.current.company_name", "type": "in", "value": [company["basic_info"]["name"]]}, {"field": "experience.employment_details.current.title", "type": "(.)", "value": "hardware engineer|embedded engineer|firmware engineer"}, ], }) company["matching_talent"] = n
# For each company on the target list, count current employees who match the profile. for company in companies: n = count_matches({ "op": "and", "conditions": [ {"field": "experience.employment_details.current.company_name", "type": "in", "value": [company["basic_info"]["name"]]}, {"field": "experience.employment_details.current.title", "type": "(.)", "value": "hardware engineer|embedded engineer|firmware engineer"}, ], }) company["matching_talent"] = n
By title or specialty
This is the subgroup cut the team kept asking for. One of their recruiters described a mechanical engineer search in San Francisco where the population splits into roughly a thousand design-focused engineers, about 500 thermal-focused, and maybe 68 in structures and analysis. The same role hides three very different supply pictures, and the right subgroup is often the small one. You get this by running the count once per specialty title, holding the location fixed.
By geography
The on-site rule is where maps live or die, because a role that reads fine nationally can be nearly empty in one metro. Counting the matching population per state or metro is what surfaces the 12-in-the-Bay-Area problem before you have promised anyone a slate. Filter on basic_profile.location.state or a city value and read the count for each place that matters.
Stack those three breakdowns and the abstract total becomes a real landscape, with the companies that employ the profile, the specialties inside the title, and the places the talent actually lives, each with a number attached.
What the finished map tells you
A finished market map is a small set of tables and counts that a client can read in a minute. The top of it is the population total and how it shrank. Under that sits the company breakdown, the title or specialty split, and the geographic distribution, each ranked by headcount.
Read together, it answers the questions a hiring manager actually has. Is this role hard or easy to fill, and the count says how hard. Where should we hunt, and the company ranking says where. Is the on-site rule realistic, and the geographic split says whether it leaves anyone to call. The map is the reality check the team described, the work that happens before sourcing and protects everything after it.
It is also what keeps a recruiter's time on the right problem. One recruiter we spoke with said her judgment belongs in the middle of the pool, where the obvious yes-or-no people at either end need none of it, and the map is what tells her how big that middle is before she opens a single profile. If you want to see one built against a role you are working now, book a demo.
From map to sourced list
The map is the analysis, and the candidate list is the deliverable it points to. Once the map tells you the market is real and shows you where to look, the next move is to turn the richest cells of it into named, contactable people.
That hand-off is clean because both runs share the same filters. The same title-and-location query that returned a total_count of 68 returns those 68 profiles when you ask for them instead of for the count. From there you rank, enrich, and export, which is its own piece of work that we cover in turning a job description into a sourced candidate list. The search engine underneath, the part that turns a plain-language role into a structured query, is covered in our guide to building a candidate sourcing engine.
The data on both sides comes from the same place. Crustdata's people and company API indexes public professional data across the open web, so the count you size the market with and the list you eventually pull are the same population at two zoom levels. Coverage is strongest where people leave a public trail and thinner where they do not, so it is worth sizing your own niche before you rely on the numbers.
Build your first market map this week
Start with one open role you are working right now. Read its job description down to the three or four filters that truly define the population, expand the title into the variants people actually use, and name the companies and stage where the profile sits.
Then size it. Run the broad count, add the location, add the seniority floor, and watch the number fall, the same move that walks a founder off an unrealistic spec. Break the survivors down by company, by specialty, and by metro, and you have a map you can put in front of a client. When the map says the market is there, hand the strongest cells to your sourcing run and pull the people.
You do not need a large team for any of this. A recruiter and one engineer can build a working version on top of an API in an afternoon, because the hard part was never the code. It was getting the counts fresh enough and segmented enough to trust in front of a client, and that is the part you can now run on demand. Crustdata is the data layer for recruiting teams that size a market before they work it. Come and see what the map looks like across your own target list. Start free with 100 credits at crustdata.com, or read more about the workflows it fits into in our recruiting solutions.
Frequently asked questions
What is a recruiting market map? A recruiting market map is a count of the talent that matches a role, broken down by employer, title, and location. It tells you how many qualified people exist, which companies they cluster in, and how the population thins as requirements tighten, so you can size the supply before you source anyone.
How is a market map different from a candidate list? A market map is aggregate and a candidate list is individual. The map counts and distributes the matching population without naming anyone, so you can judge whether a role is fillable and where to hunt. The list names the specific people you will contact, which is the step you take after the map says the market is real.
How do you build a market map from a job description? Read the description down to its real filters, expand the title into the variants people use, and name the companies the profile comes from. Then size the population with a people search count, segment the companies with a company search, and break the total down by employer, specialty, and geography until the market has a shape.
How do you size a talent pool without pulling every profile? Send your filters to a people search API and read the total count it returns, with the result limit set to one. The count reflects everyone who matches across the database, so you can size and re-size a population by adding or removing filters at almost no cost, long before you pull a single profile.
Why does the count matter when setting expectations with a hiring manager? Because the number does the arguing. When a client insists thousands of people fit the role, adding the real filters in front of them can drop the count to a few dozen, and only a handful in the metro they want on-site. Seeing the population shrink in real time resets the spec faster than any opinion can.
Products
Popular Use Cases
Competitor Comparisons
Use Cases
95 Third Street, 2nd Floor, San Francisco,
California 94103, United States of America
© 2026 Crustdata Inc.
Products
Popular Use Cases
Competitor Comparisons
Use Cases
95 Third Street, 2nd Floor, San Francisco,
California 94103, United States of America
© 2025 CrustData Inc.
Products
Popular Use Cases
Competitor Comparisons
Use Cases
95 Third Street, 2nd Floor, San Francisco,
California 94103, United States of America
© 2026 Crustdata Inc.


