I refactored things on my current project to "consolidate" the number of WSPs used, so we have a similar situation. We have around 12 Visual Studio projects but only 2 WSPs - that's just the factoring which makes the most sense and offers the most convenience for us.
I wrote some MSBuild the other day to help WSPBuilder achieve the packaging we want - this might be what you're looking for. Effectively we have 2 special "packaging" projects which consolidate assemblies, files in the SharePoint root etc by grabbing these files from other projects in the solution at compile time. This sample is for 1 of my 2 packaging projects and is saved separately as say FooCollabBuild.csproj:
<?xml version="1.0" encoding="utf-8" ?>
<Project DefaultTargets="FooSharePointBuild" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="FooSharePointBuild">
<!-- first clear down the 12 directory -->
<Message Text="------ Clearing down $(ProjectDir)12 ------" Importance="high" />
<ItemGroup>
<FilesToDelete Include="$(ProjectDir)12" />
</ItemGroup>
<Message Text="------ If an MSBuild error occurs here it can safely be ignored ------" Importance="high" />
<RemoveDir ContinueOnError="true" Directories="@(FilesToDelete)" />
<!-- now grab the 12 files from the other projects -->
<Message Text="------ Copying files into $(ProjectDir)12 ------" Importance="high" />
<ItemGroup>
<SharePointFiles Include="$(SolutionDir)Foo.SharePoint.Common\12\**\*.*" Exclude="$(SolutionDir)Foo.SharePoint.Common\12\**\*.spvdinfo" />
<SharePointFiles Include="$(SolutionDir)Foo.SharePoint.TeamSite\12\**\*.*" Exclude="$(SolutionDir)Foo.SharePoint.TeamSite\12\**\*.spvdinfo" />
<SharePointFiles Include="$(SolutionDir)Foo.SharePoint.CrossSiteCollection\12\**\*.*" Exclude="$(SolutionDir)Foo.SharePoint.CrossSiteCollection\12\**\*.spvdinfo" />
<SharePointFiles Include="$(SolutionDir)Foo.SharePoint.SubSiteCreation\12\**\*.*" Exclude="$(SolutionDir)Foo.SharePoint.SubSiteCreation\12\**\*.spvdinfo" />
<SharePointFiles Include="$(SolutionDir)Foo.SharePoint.SubSiteCreation.Workflow\12\**\*.*" Exclude="$(SolutionDir)Foo.SharePoint.SubSiteCreation.Workflow\12\**\*.spvdinfo" />
</ItemGroup>
<Copy SourceFiles="@(SharePointFiles)" DestinationFolder="$(ProjectDir)12\%(RecursiveDir)" />
<!-- now grab the assemblies from the other projects -->
<Message Text="------ Copying files into $(ProjectDir)GAC ------" Importance="high" />
<ItemGroup>
<Assemblies Include="$(SolutionDir)Foo.SharePoint.Common\bin\**\*.dll" />
<Assemblies Include="$(SolutionDir)Foo.SharePoint.TeamSite\bin\**\*.dll" />
<Assemblies Include="$(SolutionDir)Foo.SharePoint.CrossSiteCollection\bin\**\*.dll" />
<Assemblies Include="$(SolutionDir)Foo.SharePoint.SubSiteCreation\bin\**\*.dll" />
<Assemblies Include="$(SolutionDir)Foo.SharePoint.SubSiteCreation.Workflow\bin\**\*.dll" />
</ItemGroup>
<Copy SourceFiles="@(Assemblies)" DestinationFolder="$(ProjectDir)GAC\" />
</Target>
</Project>
This is then referenced in the actual .csproj file of the "package" project using:
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\FooCollabBuild.proj" />
<Target Name="AfterBuild">
<CallTarget Targets="FooSharePointBuild" />
</Target>
Then whenever I need a WSP (as opposed to copy 12/copy bin/copy GAC), I just use the WSPBuilder extensions on the package project to generate. Your SafeControls entries etc. will be taken care of since WSPBuilder sees the assemblies in the GAC folder.
Shout if you have any questions.